Commit Graph
1208 Commits
Author SHA1 Message Date
semantic-release-bot 79189ef2a4 chore(release): 7.16.0 [skip ci]
# [7.16.0](https://git.datacontroller.io/dc/dc/compare/v7.15.0...v7.16.0) (2026-09-28)

### Bug Fixes

* **ci:** correct server-ci webSourcePath and build frontend before mock deploy ([a4ad4cb](a4ad4cba28))
* **client:** parse iOS browsers in parseUserAgent ([eec3438](eec3438de9))
* **diagnostics:** give the startup service a body for browser_info ([5538e0c](5538e0c575))
* **diagnostics:** treat _debug=128 as debug on ([8de9978](8de9978213))
* **loader:** abort when a post edit hook routes to an unregistered table ([171974b](171974bd41))
* **mocks:** keep the mock hook reader inside the Drive ([7fd8ae4](7fd8ae4629))
* **sas:** drop the duplicated abort in the target re-registration check ([48d45ac](48d45ac7f8))
* **sidebar:** drop the query string from the sub-page label ([d2dd46b](d2dd46bfdf))

### Features

* **client:** report the browser and both versions in browser_info ([a770f36](a770f3695c))
* **client:** send session_info with every service request ([50282f2](50282f2ddb))
* **filters:** make the applied-filter panel expandable ([fdaa249](fdaa249f44))
* **mocks:** emulate the %mpeinit diagnostics dump ([200ee89](200ee8931d))
* **mocks:** run pre/post edit hook programs in the JS mock services ([b5f2283](b5f228351b))
* **mpeinit:** dump session_info to the log when debug is on ([fbd77a5](fbd77a5334))
* **release:** publish SHA256SUMS and verify instructions with each release ([f1ae501](f1ae501d49))
v7.16.0
2026-09-28 09:09:36 +00:00
allan d717ecd0c2 Merge pull request 'feat: support diagnostics (browser_info, browser_url_vars) for startup and hook services' (#326) from feat/browser-info into main
Release / Build-production-and-ng-test (push) Successful in 4m38s
Release / Build-and-test-development (push) Successful in 24m16s
Release / release (push) Successful in 9m2s
Reviewed-on: #326
2026-09-28 08:36:44 +00:00
dc 60ddb475d7 ci: run the hook-programs spec in the cypress job
Build / Build-and-ng-test (pull_request) Successful in 5m26s
Lighthouse Checks / lighthouse (pull_request) Successful in 21m5s
Build / Build-and-test-development (pull_request) Successful in 29m17s
hook-programs.cy.ts was in no CI spec list, so it never ran - the only
end-to-end coverage of the new mock hook execution (mockHookSource plus
the PRE/POST_EDIT_HOOK blocks) and of the loader's target re-registration
check was dead weight. Add it to the development job's --spec list.

It goes second, not last: csv-limited.cy.ts asserts the free tier and so
must keep the first slot (before any spec applies a licence key), and test
2 reads the approval queue, which is paged oldest first - a changeset
submitted by an earlier spec pushes its own row off the first page. The
spec header records the constraint. Full list verified locally: 15 specs,
132 tests, all passing.
2026-09-27 23:28:42 +00:00
dc e926649787 test(hooks): anchor the approval assertion on the grid, not a row position
The post-edit-hook spec asserted on the last row of the approval queue.
The queue also holds changesets from earlier specs in the same run, so the
position is not stable - it failed on an estate with rows left over from a
previous run while passing on a fresh one. Search every row instead.
2026-09-27 22:44:54 +00:00
dc 48d45ac7f8 fix(sas): drop the duplicated abort in the target re-registration check
%mpe_loadfail raises the message through its own %mp_abort, so the extra
%mp_abort before %return printed it twice. The newer blocks in this file
(the post-edit-hook syscc check, for instance) use %mpe_loadfail + %return
only; this block now matches them.
2026-09-27 22:44:49 +00:00
dc 7fd8ae4629 fix(mocks): keep the mock hook reader inside the Drive
mockHookSource resolves the hook value against the Drive and eval()s what
it finds. A hook value carrying '..' segments resolved outside files/ -
mock-only, but the value is hand-edited mock config. Refuse anything that
does not resolve under files/.
2026-09-27 22:44:49 +00:00
dc eec3438de9 fix(client): parse iOS browsers in parseUserAgent
CriOS, FxiOS and EdgiOS carry Safari/ as well, and the token order only
tested the desktop names, so every iPhone and iPad client was reported as
Safari with an empty version - and a hook is invited to branch on browser.

Fold the tokens into one ordered table (mobile token before its desktop
twin) so the family and its version always come from the same token, and
add the legacy Edge token (Edge/18.x) which the Edg[A-Z]?/ pattern missed.

Table-driven spec over real user agent strings: desktop Edge (both
tokens), Opera, Firefox, Chrome and Safari, Android Chrome, the three iOS
browsers, a non-browser client and an empty string. Red before the fix:
all three iOS cases returned Safari with an empty version.
2026-09-27 22:44:43 +00:00
dc 200ee8931d feat(mocks): emulate the %mpeinit diagnostics dump
Build / Build-and-ng-test (pull_request) Successful in 5m21s
Lighthouse Checks / lighthouse (pull_request) Successful in 21m19s
Build / Build-and-test-development (pull_request) Successful in 28m58s
The SAS %mpeinit macro dumps work.browser_url_vars and work.browser_info
to the log when _debug is on (2477, fields,log,trace, 131 or 128). The JS
mocks had no equivalent, so a mocked session could not show what the
client sent.

Add mpeinit() to dcMockUtils.js and call it at the bottom of the file -
the same place the SAS services call %mpeinit - so every mock service that
eval()s the utilities dumps the two input tables. console.log output is
returned in the request log, so it lands in Data Controller's SAS Log tab
exactly as the SAS version does.

The tables are read with fetchRaw, so both the JSON body shape and the
multipart _WEBIN_NAME / _WEBIN_FILEREF shape work. Values are dumped with
the same NOTE: prefixes and name=value formatting as the macro.
2026-09-27 22:08:31 +00:00
allan 10122312cb Merge pull request 'fix(ci): correct server-ci webSourcePath and build the frontend before the mock deploy' (#332) from fix/server-ci-streamweb into feat/browser-info
Build / Build-and-ng-test (pull_request) Successful in 5m58s
Lighthouse Checks / lighthouse (pull_request) Successful in 21m46s
Build / Build-and-test-development (pull_request) Successful in 30m0s
Reviewed-on: #332
2026-09-27 19:08:23 +00:00
dc ce1de3d5b4 ci: run the browser-info spec in the cypress job
Build / Build-and-ng-test (pull_request) Successful in 5m58s
Lighthouse Checks / lighthouse (pull_request) Successful in 22m2s
Build / Build-and-test-development (pull_request) Successful in 27m40s
browser-info.cy.ts asserts the diagnostics tables on the adapter payload but was not in the job's explicit --spec list, so it never ran. Append it so the feature is covered by CI.
2026-09-27 19:02:40 +00:00
dc fbbaa0ac5e docs(diagnostics): correct the browser_url_vars collision comment
The comment claimed the hash query string is read first and therefore wins on a name collision. collectBrowserUrlVars actually reads window.location.search first and dedupes with a seen set, so the first occurrence wins and that is the search string value. Describe the behaviour the code has.
2026-09-27 19:02:30 +00:00
dc a4ad4cba28 fix(ci): correct server-ci webSourcePath and build frontend before mock deploy
Build / Build-and-ng-test (pull_request) Successful in 5m42s
Lighthouse Checks / lighthouse (pull_request) Successful in 21m28s
Build / Build-and-test-development (pull_request) Successful in 28m23s
The server-ci target streams the Angular frontend (CONTRIBUTING.md documents
http://localhost:5000/AppStream/clickme/), but webSourcePath was
"`../../../../client/dist". The leading backtick makes the first path segment
literal, so it happened to resolve to <repo>/client/dist by accident; the
correct value relative to the sasjs project root (sas/mocks) is
../../client/dist.

The real problem was that CI job 2 never built the client, so client/dist did
not exist when the mock services were deployed: the deploy logged
"webSourcePath: <repo>/client/dist present in 'streamConfig' doesn't exist"
and streamed no frontend, while the job still passed. Build the production
frontend before deploying the mocks so the streamed app is actually produced.
2026-09-27 18:48:03 +00:00
dc 5538e0c575 fix(diagnostics): give the startup service a body for browser_info
Build / Build-and-ng-test (pull_request) Successful in 6m6s
Lighthouse Checks / lighthouse (pull_request) Successful in 21m55s
Build / Build-and-test-development (pull_request) Successful in 26m43s
Both startupservice call sites pass null, and the attach guard skipped a
null payload, so the service whose log a support ticket is read from
never received browser_info / browser_url_vars.

The e2e test named for the startup service only asserted on getdata, so
the gap was invisible. The name is corrected to what it asserts, its
sibling sweep now excludes the diagnostics services (it must, once the
startup service really does carry them), and the guard itself is covered
by a unit spec - red before the fix, green after.
2026-09-25 15:57:53 +00:00
allan 11b6c23000 Merge pull request 'feat(filters): make the applied-filter panel expandable' (#331) from feat/expandable-filter-panel into feat/browser-info
Build / Build-and-ng-test (pull_request) Successful in 6m8s
Lighthouse Checks / lighthouse (pull_request) Successful in 21m56s
Build / Build-and-test-development (pull_request) Successful in 27m11s
Reviewed-on: #331
2026-09-25 15:16:00 +00:00
dc ba21ff00bc test(filters): wait for the editor before shooting its panel
Build / Build-and-ng-test (pull_request) Successful in 6m14s
Lighthouse Checks / lighthouse (pull_request) Successful in 21m51s
Build / Build-and-test-development (pull_request) Successful in 28m0s
The editor screenshot was taken during the transition from the viewer, so it
captured the viewer's panel - byte-identical to the viewer shot. Wait for the
editor-only chrome (.editor-title, .btnCtrl) before asserting or shooting.
2026-09-25 14:58:14 +00:00
dc 65a96922f6 style(filters): set the filter clause in a monospace face, and shoot the editor panel
Build / Build-and-ng-test (pull_request) Successful in 6m14s
Lighthouse Checks / lighthouse (pull_request) Successful in 22m11s
Build / Build-and-test-development (pull_request) Successful in 27m43s
The panel shows a query, so set it in the monospace face the app already uses
for code-like content (SAS logs, cell text) rather than the UI font.

The spec now also captures the editor's panel - collapsed and expanded - since
the editor is where the clause was previously clamped with an ellipsis.
2026-09-25 14:49:31 +00:00
dc 92bc72f51b ci: run the filter-panel spec in the cypress job
Build / Build-and-ng-test (pull_request) Successful in 5m41s
Lighthouse Checks / lighthouse (pull_request) Successful in 22m26s
Build / Build-and-test-development (pull_request) Successful in 28m15s
2026-09-25 14:30:30 +00:00
dc fdaa249f44 feat(filters): make the applied-filter panel expandable
Build / Build-and-ng-test (pull_request) Successful in 5m45s
Lighthouse Checks / lighthouse (pull_request) Successful in 21m28s
Build / Build-and-test-development (pull_request) Successful in 26m36s
A filter clause is shown in a panel above the grid. The panel was clamped to a
single line - in the editor with an ellipsis, and in the viewer by letting the
clause run the full width of the table - so a long or complex filter could not
be read on screen.

The panel is now collapsed to one line and carries a chevron that expands it to
show the whole clause, wrapped, and collapses it again. The chevron is rendered
only when the clause does not fit the collapsed line, so a short filter looks
exactly as it did before. Whether it fits is measured from the DOM rather than
guessed from the length of the text, since it depends on the rendered width; the
measurement is deferred out of the change-detection cycle, and re-run when the
clause, the available width, or the state changes.

This replaces the editor's hover-only reveal with a real control, which is a
button carrying aria-expanded and an accessible label, and gives the viewer the
same affordance. Both panels share the styling, which was previously duplicated
between the two component blocks.

Tested with `client/cypress/e2e/filter-panel.cy.ts`, at a laptop-sized viewport,
covering the three states:

1. no filter - the panel is not rendered
2. a short filter - shown in full, with no chevron
3. a long filter (an IN over every value of a free-text column, 646 characters)
   - collapsed with a chevron, expands to the whole clause over several lines,
   and collapses again
4. the editor panel behaves the same way for the same clause
2026-09-25 14:27:02 +00:00
allan 9c51fff0ef Merge pull request 'chore(deploy): drop the hardcoded dcPath default from the frontend template' (#330) from chore/drop-dcpath-default into feat/browser-info
Build / Build-and-ng-test (pull_request) Successful in 6m2s
Lighthouse Checks / lighthouse (pull_request) Successful in 22m30s
Build / Build-and-test-development (pull_request) Successful in 26m24s
Reviewed-on: #330
2026-09-25 13:28:46 +00:00
dc 5299e0ba06 chore(deploy): drop the hardcoded dcPath default from the frontend template
Build / Build-and-ng-test (pull_request) Successful in 5m14s
Lighthouse Checks / lighthouse (pull_request) Successful in 22m16s
Build / Build-and-test-development (pull_request) Successful in 26m11s
The `dcPath` attribute in the `<sasjs>` tag of `client/src/index.html` is a
relic of the older deployment approach, where a `viya.json` was shipped inside
the frontend bundle and read the path from the tag.

Nothing depends on the value: the deploy screens take the deployment path from
the user - the SASjs configurator derives a platform-appropriate default from
`SYSSCPL`, the automatic Viya flow sets `/export/viya/homes/<user>`, and both
the automatic and manual screens expose it as an editable DCLOC field.

Leaving `/tmp/dc` in the template is actively misleading: it is a valid-looking
path that a Viya deployment cannot necessarily write to, and because a deploy
re-streams the frontend from this file, the stale value lands in the deployed
`DC.html` where the deploy screen picks it up.

The attribute remains readable via `getAppAttribute('dcPath')` for anyone who
wants to pin it, so this only removes the shipped default.
2026-09-25 13:12:08 +00:00
allan 25077e9ded Merge pull request 'fix(sidebar): drop the query string from the sub-page label' (#329) from fix/sidebar-subpage-query-string into feat/browser-info
Build / Build-and-ng-test (pull_request) Successful in 5m36s
Lighthouse Checks / lighthouse (pull_request) Successful in 21m50s
Build / Build-and-test-development (pull_request) Successful in 26m3s
Reviewed-on: #329
2026-09-25 09:46:21 +00:00
dc d2dd46bfdf fix(sidebar): drop the query string from the sub-page label
Build / Build-and-ng-test (pull_request) Successful in 5m25s
Lighthouse Checks / lighthouse (pull_request) Successful in 21m16s
Build / Build-and-test-development (pull_request) Successful in 26m18s
Router.url carries the query string, so getSubPage() returned e.g.
'tables?embed=va' for a route with any parameter, and the sidebar label
rendered 'TABLES?EMBED=VA'. A VA report embed always adds a parameter, so
that is the case it shows up in.

Take the path part of the segment before splitting. Covered by a spec that
fails without the change.
2026-09-25 09:42:57 +00:00
dc 8de9978213 fix(diagnostics): treat _debug=128 as debug on
Build / Build-and-ng-test (pull_request) Successful in 5m24s
Lighthouse Checks / lighthouse (pull_request) Successful in 20m58s
Build / Build-and-test-development (pull_request) Successful in 25m23s
The adapter sends _debug=128 rather than 131 on the Viya web (JES) path
when runAsTask is enabled - which is the path the frontend uses there -
so the extra mpeinit logging, including the browser_info /
browser_url_vars dump, never fired on those estates. Accept 128
alongside 131 (and the existing 2477 / fields,log,trace values).

Verified on a Viya estate: the same service with _debug=128 logs
'no work.browser_info on this request' with the fix and logs nothing
without it.
2026-09-25 08:22:14 +00:00
dc 293636a5f8 refactor(client): scope browser_info to startup and hook services, add browser_url_vars
Build / Build-and-ng-test (pull_request) Successful in 5m24s
Lighthouse Checks / lighthouse (pull_request) Successful in 21m15s
Build / Build-and-test-development (pull_request) Successful in 25m11s
The diagnostics tables now go only where they can be read: the startup
service, and the services that %include customer-provided code - the
hook scripts (getdata, stagedata, loadfile, restore, postdata) and the
dynamic cell dropdown programs (getdynamiccolvals). Every other service
is spared the payload, so the high-frequency calls stay lean.

The page's URL parameters now also travel as a browser_url_vars table,
one row per parameter (name, value), taken from both the search string
and the hash query string - easier for a SAS developer than parsing the
browser_info url column. The table is sent only when the URL has at
least one parameter, and only to the same services as browser_info.

mpeinit's debug dump covers browser_url_vars alongside browser_info.

The spec asserts both directions on the adapter interface: the viewer's
data services carry no browser_info, getdata does, and a ?labels=true
visit arrives as a browser_url_vars row. Each test boots from the app
root with a guarded evaluation-agreement acceptance, because Cypress
clears cookies between tests and the SASjs Server session drops (the
same flake hook-programs.cy.ts hits on this estate).
2026-09-25 00:26:16 +00:00
allan 1e8a261ada Merge pull request 'feat(release): publish SHA256SUMS and verify instructions with each release' (#327) from ci/release-asset-hashes into feat/browser-info
Build / Build-and-ng-test (pull_request) Successful in 5m27s
Lighthouse Checks / lighthouse (pull_request) Successful in 21m14s
Build / Build-and-test-development (pull_request) Successful in 25m32s
Reviewed-on: #327
2026-09-24 23:07:36 +00:00
dc f1ae501d49 feat(release): publish SHA256SUMS and verify instructions with each release
Build / Build-and-ng-test (pull_request) Successful in 5m20s
Lighthouse Checks / lighthouse (pull_request) Successful in 21m10s
Build / Build-and-test-development (pull_request) Successful in 25m20s
The release job now hashes every asset it uploads (frontend.zip, the SAS 9
and Viya deployment programs, the SASjs Server bundle) into a SHA256SUMS
file with basename-only paths, and attaches it to the release alongside
the assets. The release body gains a "Verifying the download" section -
emitted by .gitea/scripts/verify-section.sh so the YAML scalar stays
clean - with the sha256sum -c command and a note on what a checksum does
and does not prove.

The upload loop is rewritten from an inline list to a bash array shared
by the hashing and upload steps, so the asset set cannot drift between
what is hashed and what is uploaded.

Simulated end to end locally (stubbed curl/jq): 8 uploads fire with
correct paths, SHA256SUMS verifies with sha256sum -c, a tampered file
fails the check, and the assembled release body keeps the existing
notes with the new section and installation footer on their own lines.
2026-09-24 22:30:36 +00:00
allan 9d740f9f47 Merge pull request 'fix(loader): abort when a post edit hook routes to an unregistered table' (#325) from fix/validate-post-edit-hook-target into feat/browser-info
Build / Build-and-ng-test (pull_request) Successful in 5m57s
Lighthouse Checks / lighthouse (pull_request) Successful in 22m20s
Build / Build-and-test-development (pull_request) Successful in 25m54s
Reviewed-on: #325
2026-09-24 21:03:52 +00:00
allan 76dd4bf69a Update sas/sasjs/macros/mpeinit.sas
Build / Build-and-ng-test (pull_request) Successful in 6m1s
Lighthouse Checks / lighthouse (pull_request) Successful in 22m22s
Build / Build-and-test-development (pull_request) Successful in 26m33s
2026-09-24 20:45:43 +00:00
dc fed8c7344e refactor(client): drop the VA fields from browser_info
Build / Build-and-ng-test (pull_request) Successful in 5m16s
Lighthouse Checks / lighthouse (pull_request) Successful in 21m3s
Build / Build-and-test-development (pull_request) Successful in 25m59s
browser_info is support diagnostics: where the request came from and what
client sent it. The VA data-driven content metadata belonged to the embed
feature, not to diagnostics, so the va_ fields and the va_parameters and
va_columns tables go. The editor still reads the VA message for its own
feature work; the diagnostics table no longer does.
2026-09-24 20:28:51 +00:00
dc a770f3695c feat(client): report the browser and both versions in browser_info
Build / Build-and-ng-test (pull_request) Successful in 5m15s
Lighthouse Checks / lighthouse (pull_request) Successful in 21m12s
Build / Build-and-test-development (pull_request) Successful in 25m12s
Adds dc_version, adapter_version, browser, browser_version, platform and the
raw user_agent to the browser_info row, so a support ticket can be read off
the log and a hook can branch on the browser without parsing the user agent
itself. The summary comes from a new parse-user-agent util, following the
existing shared/utils convention.

Also adds a Cypress spec asserting the table reaches the adapter interface,
which is the only place the payload is observable end to end.
2026-09-24 19:31:13 +00:00
dc 7abd260f98 refactor: rename session_info to browser_info
'session' reads as the SAS session in this codebase; the row is what the
browser reports about itself and its context (page URL, referrer,
timezone, locale, client version) plus what Visual Analytics told it.
Checked for collisions: no session_info, browser_info or session_results
anywhere in the repo, and the adapter only reserves the $-prefixed
formats table.
2026-09-24 18:12:55 +00:00
dc fbd77a5334 feat(mpeinit): dump session_info to the log when debug is on
Guard on the table existing, since a service called directly, or by a
client that predates this change, will not send one.
2026-09-24 18:10:07 +00:00
dc 50282f2ddb feat(client): send session_info with every service request
Adds a single-row `session_info` input table to every service call made
through SasService.request, describing where the request came from:

- url - the URL of the Data Controller page itself (the iframe), not the
  document embedding it, so an embedded report and any parameters its
  author added to the embed URL can be told apart
- referrer - the embedding document
- timezone / tz_offset / locale - the browser's, so services and hooks can
  match date logic and labels to what the user sees
- version - the client build, for support and version-aware hooks
- va_result_name / va_row_count / va_available_row_count - VA data-driven
  content metadata, blank and zero when not embedded in VA

When VA is present its parameters and columns are sent too, as
`va_parameters` and `va_columns` tables, so a hook script can branch on
the report's own parameters.

The table is always sent - when the app is standalone the VA fields are
simply empty. Hook scripts read it directly from WORK; no service needs to
change.
2026-09-24 18:05:53 +00:00
allan d61308578c Merge pull request 'feat(mocks): run pre/post edit hook programs in the JS mock services' (#324) from feat/mock-hook-programs into fix/validate-post-edit-hook-target
Build / Build-and-ng-test (pull_request) Successful in 5m44s
Lighthouse Checks / lighthouse (pull_request) Successful in 23m3s
Build / Build-and-test-development (pull_request) Successful in 27m27s
Reviewed-on: #324
2026-09-24 16:11:47 +00:00
dc 171974bd41 fix(loader): abort when a post edit hook routes to an unregistered table
Build / Build-and-ng-test (pull_request) Successful in 5m48s
Lighthouse Checks / lighthouse (pull_request) Successful in 22m13s
Build / Build-and-test-development (pull_request) Successful in 25m15s
A POST_EDIT_HOOK may re-point a changeset at a different table - that is what
lets an empty mirror stand in for a real one. Nothing checked the result, so a
hook pointing at a table with no MPE_TABLES row produced a changeset that could
not be reviewed: mpe_checkrestore resolves the audit table from that row, and
with none found its `select count(*) into: chk from &audtab` collapses to
`from where ...` - ERROR 22-322, syscc=1012, and the approval screen aborts
with a syntax error rather than an explanation.

Validate the target immediately after the hook, while the submitter is still
watching, and fail with a message naming the table and the reason. The target
table must be registered in MPE_TABLES; a hook may only route to a registered
table.

Verified on the test estate against the deployed stagedata service:
- target unregistered -> job cancels with
  'ERROR: target table DCHOOK.ORDERS is not registered in VIYA0846.mpe_tables -
  a post edit hook may only route a changeset to a registered table'
  (MPE_LOADFAIL STATUS 'FAILED - TARGET NOT REGISTERED')
- target registered -> the same submit returns STATUS SUCCESS
2026-09-24 15:45:24 +00:00
dc b5f228351b feat(mocks): run pre/post edit hook programs in the JS mock services
Build / Build-and-ng-test (pull_request) Successful in 5m41s
Lighthouse Checks / lighthouse (pull_request) Successful in 22m52s
Build / Build-and-test-development (pull_request) Successful in 25m48s
The JS mock services carried MPE_TABLES.pre_edit_hook / post_edit_hook values
but never acted on them, so a table configured with hooks behaved differently
in the mock than in production.

Add mockHookSource() to dcMockUtils (resolving a hook program name to the
source of its .js counterpart on the Drive) and run the hook where the SAS
backend does:

- editors/getdata runs the pre-edit hook after filtering, letting it reassign
  visibleRows / visibleColumns (the work.OUT contract) - so an empty mirror can
  display the live rows of the table it points at
- editors/stagedata and editors/loadfile run the post-edit hook before the
  MPE_SUBMIT row is written, letting it reassign libref / dsn (the call
  symputx contract) - so a changeset submitted against a mirror is raised
  against the real table

Seed a demo pair in makedata (TESTDATA.DEMO_ORDERS plus an empty
TESTDATA.DEMO_MIRROR with both hooks) and cover it with a Cypress spec.
2026-09-24 15:17:07 +00:00
semantic-release-bot f1734a2de0 chore(release): 7.15.0 [skip ci]
# [7.15.0](https://git.datacontroller.io/dc/dc/compare/v7.14.2...v7.15.0) (2026-09-23)

### Bug Fixes

* **mocks:** keep special missings in the DIFF, and add the clip recording spec ([d124ce3](d124ce3b36))
* **mocks:** list a column's own special missing in its dropdown ([b8f16a6](b8f16a6382))
* **mocks:** make the approval path work, and record the approval scene ([12847cf](12847cf071))
* **validator:** a range rule ignores a missing value ([70dae4b](70dae4b701))
* **validator:** keep the regular missing in a numeric dropdown source too ([a639bca](a639bca702))
* **validator:** primary keys are NOT NULL, and a strict dropdown can match a special missing ([586a41a](586a41ad49))
* **validator:** reject a special missing on a NOT NULL column ([6d85bce](6d85bce2a0))

### Features

* **validator:** compare MINVAL and MAXVAL in SAS's own order ([3467322](3467322e99))
v7.15.0
2026-09-23 23:08:14 +00:00
allan ecf6dc03df Merge pull request 'feat(validator): compare MINVAL and MAXVAL in SAS's order, with the demo table and clip' (#323) from mocks/rules-demo-table into main
Release / Build-production-and-ng-test (push) Successful in 4m46s
Release / Build-and-test-development (push) Successful in 24m0s
Release / release (push) Successful in 8m57s
Reviewed-on: #323
2026-09-23 22:35:35 +00:00
dc 283bf067c5 test(mocks): a missing range on the demo table, and the ordering beat in the clip
Build / Build-and-ng-test (pull_request) Successful in 6m8s
Lighthouse Checks / lighthouse (pull_request) Successful in 22m33s
Build / Build-and-test-development (pull_request) Successful in 26m23s
DEMO_01 gains a GRADE column carrying MINVAL .A with MAXVAL .C, so the demo table
can show a range written in special missings: .B is inside it and .D is outside.
The clip's range-rule scene is rebuilt around the order - the same special missing
fails AMOUNT (MINVAL 1, below every number), passes SCORE (MAXVAL 100, below the
ceiling), and in GRADE .B is taken while .D is refused.

The captions from the previous take claimed a range rule steps aside for a
missing value, which the engine no longer does, so they are corrected along with
the scene.
2026-09-23 21:27:42 +00:00
dc 3467322e99 feat(validator): compare MINVAL and MAXVAL in SAS's own order
A range rule coerced both the cell value and its own value with parseFloat, so a
special missing in either place compared as NaN. MINVAL rejected a blank and a
special missing and MAXVAL accepted both, which made the two rules disagree with
each other, and a range written in special missings - MINVAL .A with MAXVAL .C -
compared nothing at all: every number failed and every missing passed, so .D was
as acceptable as .B.

Both sides are now keyed into the order SAS itself uses for a numeric variable:
every missing sorts below every non-missing value, and the missing values are
ordered ._ then the regular missing then .A through .Z. MINVAL .A with MAXVAL .C
therefore takes .B and refuses .D, a blank fails a floor of .A, and a number sorts
above every missing - it passes a floor of .A and fails a ceiling of .C.

This supersedes the earlier "a range rule ignores a missing value" change in this
branch: a missing is not outside the order, it is at the bottom of it.

A rule value that is neither a number nor a special missing satisfies nothing, so
the column fails until the rule is corrected.
2026-09-23 21:27:28 +00:00
dc 101087613e test(mocks): demo the corrected range-rule behaviour, and correct the captions
Build / Build-and-ng-test (pull_request) Successful in 5m53s
Lighthouse Checks / lighthouse (pull_request) Successful in 22m12s
Build / Build-and-test-development (pull_request) Successful in 27m3s
DEMO_01 no longer carries a NOTNULL rule for ID: it is the table's buskey, and
Data Controller applies NOT NULL to a primary key by itself, so the demo now
shows the behaviour rather than a rule that duplicates it.

The clip's range-rule scene showed a special missing being rejected by MINVAL.
It now shows the missing accepted in both AMOUNT (MINVAL 1) and SCORE (MAXVAL
100), and a real number out of range - 0, then 200 - rejected.

Two captions were also wrong and are corrected: the period in a special missing
is optional (the demo's own row 2 holds .a), and ID is NOT NULL because it is the
primary key.
2026-09-23 21:00:45 +00:00
dc 70dae4b701 fix(validator): a range rule ignores a missing value
MINVAL rejected a blank and a special missing, so a column carrying a minimum
could not hold either - and a special missing was unusable in it. MAXVAL has
always accepted both, so the two rules disagreed with each other as well as with
the point of a range rule: a minimum or a maximum constrains a number, and a
missing value is not a number. NOTNULL is the rule that rejects a missing.

MINVAL now returns true for a missing value, matching MAXVAL, and the tests for
both rules cover a blank, undefined and a special missing.
2026-09-23 21:00:26 +00:00
dc a36f0d5184 test(mocks): caption the clip from a track the spec writes as it runs
Build / Build-and-ng-test (pull_request) Successful in 5m11s
Lighthouse Checks / lighthouse (pull_request) Successful in 21m20s
Build / Build-and-test-development (pull_request) Successful in 25m10s
Every beat now says what it shows and why the rule behaves as it does, so the
colour of a cell never has to be inferred. The spec appends a caption track as
it runs - a label, the text, and a wall-clock stamp per beat - and the encoder
turns consecutive entries into subtitle cues, calibrated against the one thing
measurable in the video (the amber SOFTREGEX warning cell appearing).

The timestamp is taken inside a cy.then(): Cypress evaluates a command's
arguments when the command is queued, so Date.now() passed to cy.writeFile
directly gives every mark the same value - the moment the spec body ran.

Two beats also gained room so their caption can be read: the opening grid hold,
and the DIFF hold before the staged screen. A mark with empty text clears the
caption while the clip moves between screens.
2026-09-23 16:55:36 +00:00
dc b6b0d3d343 test(mocks): record the clip as one editing session, with the staged screen
Build / Build-and-ng-test (pull_request) Successful in 5m14s
Lighthouse Checks / lighthouse (pull_request) Successful in 21m10s
Build / Build-and-test-development (pull_request) Successful in 25m10s
The clip reloaded the app halfway through, because the second half began with a
cy.visit - which reads as the table refreshing for no reason. It is now a single
session: the table is opened once, every rule scene is played on row 1 and each
value is put back, the real change is submitted, and the review screens are
reached through the app's own navigation. Nothing reloads.

Because the rule scenes are put back first, the DIFF carries only the two cells
the clip is about, and the spec asserts that: RATING and LAST_REVIEWED carry the
changed-cell marker and the other cells do not.

The staged-data screen gets a beat of its own (view staged data, held long
enough to read), since that is the row the approval acts on - and its assertion
checks text that only exists on that screen, because asserting 'Staged Data'
would have been satisfied by the button that was just clicked.

Also shortens the correction beats, which do not need a study pause.
2026-09-23 16:21:56 +00:00
dc 5cea685405 test(mocks): hover the clip's DIFF cells with the real mouse, and tighten the beats
Build / Build-and-ng-test (pull_request) Successful in 5m25s
Lighthouse Checks / lighthouse (pull_request) Successful in 21m6s
Build / Build-and-test-development (pull_request) Successful in 25m8s
The clip hovered a changed DIFF cell with trigger('mouseover') and then asserted
contain.text on the tooltip. Clarity reveals a tooltip through CSS :hover, which
a synthetic event does not activate, so the tooltip stayed visibility: hidden
while the assertion passed anyway - the text is in the DOM either way. The
recording therefore showed no previous value at all.

hoverDiffCell now moves the real mouse (cypress-real-events, already a
dependency) and asserts the tooltip's computed visibility and opacity alongside
its text, so a take that does not actually display it fails. The first real
mouse move after another action can be swallowed, hence the repeat.

The holds are cut again as well - the recording is 82s where it was 101s - since
the finished clip is played at 1.43x on the way out.
2026-09-23 15:41:00 +00:00
dc b8f16a6382 fix(mocks): list a column's own special missing in its dropdown
Build / Build-and-ng-test (pull_request) Successful in 5m23s
Lighthouse Checks / lighthouse (pull_request) Successful in 21m14s
Build / Build-and-test-development (pull_request) Successful in 25m16s
A SOFTSELECT/HARDSELECT whose rule value is a library.member.column reference
takes its list from that column, and getdata.sas builds it with cats() and then
orders it by the column itself. The mock sent the raw stored value instead, so a
numeric special missing reached the client in its period form (".a") rather than
as the bare letter cats() produces, and it sat in row order rather than below
every number. It now does both, so a dropdown sourced from a column reads the
way the real one does.

DEMO_01 gains a STATUS column carrying a SOFTSELECT over its own column, with
one row holding a special missing, so the dropdown can be demonstrated listing
that missing alongside the ordinary values.

The clip spec records that beat, hovers both changed cells on the review screen
so the value each replaced is on screen, and cuts the waits that were only
letting a page or a modal settle.
2026-09-23 15:11:27 +00:00
dc 12847cf071 fix(mocks): make the approval path work, and record the approval scene
Build / Build-and-ng-test (pull_request) Successful in 5m28s
Lighthouse Checks / lighthouse (pull_request) Successful in 21m4s
Build / Build-and-test-development (pull_request) Successful in 25m22s
The mock's APPROVE_TABLE branch called loadTableData(), which is not defined in
that service's scope - the loader it defines is mpeLoadTableData(). Every ACCEPT
therefore failed with "No webout was returned by job", and it went unnoticed
because the demo clip stopped at the DIFF. With the loader corrected, ACCEPT
applies the load to the base table, writes the MPE_REVIEW / MPE_SUBMIT /
MPE_DATALOADS entries and lands on the history page.

DEMO_01 row 2 now carries a special missing (RATING .a) so a demo can show one
special missing changing into another - the DIFF compares the two rather than
treating either as a blank.

The clip spec gains the two beats the companion post describes: the approver
opens the submission from the approvals list, switches the DIFF between
formatted and unformatted (15JAN2026 / 24121 on the date9. column), accepts,
and the history shows the change APPROVED. Both DIFF beats scroll the table
right, because the changed columns sit off the edge of a 1280-wide frame. The
key entered in the NOTNULL scene is now a value that is not already a key, since
the editor checks the keys before it checks the rules and would report the
duplicate instead of the invalid value.
2026-09-23 13:50:15 +00:00
dc a639bca702 fix(validator): keep the regular missing in a numeric dropdown source too
Build / Build-and-ng-test (pull_request) Successful in 6m2s
Lighthouse Checks / lighthouse (pull_request) Successful in 22m0s
Build / Build-and-test-development (pull_request) Successful in 26m9s
The previous commit kept a special missing as its bare letter but still
sent the regular missing (".") through Number(), so it landed in the
dropdown source as NaN.  Both now stay as they are: the dropdown lists
".", "_" and "A" alongside the ordinary values, with no NaN entries.

Also guards addPrimaryKeyNotNullRules against a stale buskey naming a
column the table no longer has - a rule is only synthesised for a column
that is actually in the grid.
2026-09-23 08:36:53 +00:00
dc 586a41ad49 fix(validator): primary keys are NOT NULL, and a strict dropdown can match a special missing
Build / Build-and-ng-test (pull_request) Successful in 5m17s
Lighthouse Checks / lighthouse (pull_request) Successful in 21m33s
Build / Build-and-test-development (pull_request) Successful in 25m47s
Two special-missing gaps in the DQ validator:

- A primary key column is NOT NULL by definition, but nothing enforced it
  unless the target table carried a physical constraint or MPE_VALIDATIONS
  had a NOTNULL rule for the column - so a blank or a special missing could
  sit in the key.  The constructor now synthesises a NOTNULL rule for every
  PK column that lacks one, so the grid, the edit-record modal and Excel
  upload validation all reject both.

- getDqDropdownSource() ran Number() over every entry for a numeric column,
  turning the bare letter a special missing arrives as ("A", "_") into NaN.
  A strict HARDSELECT dropdown could therefore never accept a value the
  column actually holds.  The letter is now kept; the regular missing (".")
  still goes through Number(), since a blank cell is governed by allowEmpty.

The constructor also copies dqRules rather than aliasing the caller's array,
which had been leaking the rules updateDqData() derives into the shared
fixture.
2026-09-23 08:19:21 +00:00
dc 6d85bce2a0 fix(validator): reject a special missing on a NOT NULL column
Build / Build-and-ng-test (pull_request) Successful in 5m50s
Lighthouse Checks / lighthouse (pull_request) Successful in 21m41s
Build / Build-and-test-development (pull_request) Successful in 25m18s
A special missing is NULL as far as a SAS NOT NULL (or primary key)
constraint is concerned - an insert carrying one is rejected with an
integrity constraint error (_NM0001_ / _PK0001_) - but the frontend
NOTNULL rule passed it, so the editor accepted a value the target table
refuses.  getdata merges a physical NOT NULL constraint into a NOTNULL
rule, so the mismatch was reachable on any table with the constraint.

The rule now fails a special missing on a numeric column.  A single
letter stays valid on a character column, where there is no special
missing concept.

Also updates the DEMO_01 clip script: the NOTNULL scene now shows a
blank AND a special missing both rejected, with a number accepted.
2026-09-23 07:45:18 +00:00