Enables noMacroIfInOpenCode and promotes it to "error". Open-code %if is a SAS 9.4M5 feature, so the 19 sites it found in nine files were hiding a version requirement the codebase does not document (the requirements page says 9.4M3). Each is rewritten so the construct no longer appears in open code: - Six blocks become a small macro called unconditionally, which is where the conditional then lives: drop_cas_tables (demodata), add_derived_cols (getdata), build_sasdata4 (getdata test 4), drop_derived (stagedata), build_staged (getrawdata), and one per option in the 20261003 migration. - Four blocks in the two makedata STPs get the same treatment: check_outfmt, check_keepme, emit_response and self_destruct. Their `call symputx` writes are now explicit about the global symbol table, because a %let or a plain symput inside a wrapper would be local to it. - Two guards in stagedata and getrawdata are removed rather than wrapped: the SQL only selects into a list that is already %let blank, and with no derived variables it returns no rows, so the guard was doing nothing. The trailing drop keeps a conditional, now in a macro and tested with %trim, since a zero-row select can leave a single blank in the variable. - validatefilter's two %let branches become one data step with call symputx, which writes to the global table the later %mp_filterstore reads. Verified: `npm run lint:sas` clean with the rule as an error; `sasjs compile` succeeds; on sas.4gl.io the full suite passes 64/64, including the derived variable tests in getdata test 4, stagedata tests 3 and 4 and validatefilter, which are the paths these wrappers touch.
6.2 KiB
Agent Instructions
Read CONTEXT.md at the repo root first - it is the domain glossary and orientation for Data Controller (roles, load types, MPE control tables, validations, security). Use its vocabulary in your output.
Related repositories
Data Controller spans three sibling repos (usually checked out side by side):
dc(this repo) - the product source (Angular client + SAS backend).docs.datacontroller.io- the user-facing product documentation (MkDocs). User-facing behaviour is documented there; the.agents/docs/deep-dives here link out to it.datacontroller.io- the marketing site, blog and feed (Gatsby).
Git
Do NOT auto-commit. Never run git commit (or git push) unless the user explicitly asks. Leave changes in the working tree for the user to review and commit themselves.
CHANGELOG and versioning
Do NOT edit CHANGELOG.md by hand, and do NOT bump the version in package.json. The release pipeline (semantic-release, in .gitea/workflows/release.yaml) generates both automatically from Conventional Commit messages on every push to main. Manual entries collide with the pipeline's release commit and are overwritten anyway. Control the changelog through your commit messages instead. See .agents/docs/releases-and-changelog.md.
Markdown files
Do NOT hard-wrap Markdown files (no fixed-width line wrapping / carriage returns inside paragraphs). Each paragraph, list item, and heading should be a single logical line, regardless of length. Let the editor/viewer soft-wrap. This includes AGENTS.md itself.
This is different from .sas files, where a maximum line length applies. The no-wrap rule applies to all *.md files in this repo (docs, READMEs, etc.).
Rationale: hard-wrapped prose produces noisy diffs when sentences are edited and reflowed, and Markdown renderers already handle wrapping.
Linting (required before "done")
Never consider a change complete until the relevant linters pass on the files you touched — do not rely on the user's pre-commit hooks to catch it:
- Client (TypeScript/HTML/etc.): run
npm run lint:checkfrom theclient/directory (prettier). Fix any failures withnpm run lint:fix. - SAS: after creating or modifying any
.sasfiles, runnpm run lint:sasfrom thesas/directory. That script runs the@sasjs/cliversion pinned insas/package.json, so it matches CI; a global ornpxsasjsmay resolve a different@sasjs/lintand report a different rule set. Rules are promoted toerrorinsas/.sasjslintone at a time as their findings reach zero, and only errors change the exit code - so the whole project must stay clean of the promoted rules, not just the files you touched. Warnings under rules not yet promoted are the remaining backlog; do not add to them in files you touch, and do not clear other files' findings in the same change unless that rule is the point of the change.sas/.sasjslintcarriesignoredMacros: ["webout", "mpeinit", "mpeterm"].weboutis the linter's own default -sasjs compileinjects%weboutinto every service - and an explicit list replaces that default, so it has to be restated there.mpeinitandmpetermare listed because every service is compiled with a serviceInit program (sasjs/utils/serviceinitsas9|viya|server.sas) that declares both macros in its own header, so a service body may call them without declaring them. Add a macro to that list only when it reaches a file the same way - injected by the compiler, through serviceInit, testSetUp or another implicit provider - never to silence a genuinely missing declaration.noBannedStringsflags the literal wordserrorandwarninganywhere in a file, comments included. Reword prose; where the text has to stay (a real%put, a doxygen@warningtag), keep the log output identical with%put %str(WARN)ING:rather than a per-file override, and use@sasjslint {"noBannedStrings": false}only when the word itself is the content. Hook scripts (sasjs/services/hooks/*.sas) run inside the service that includes them, so the macros they call come from that service's session. They carry a file-local@sasjslint {"ignoredMacros": [...]}rather than a macro list of their own, because end users copy and edit these scripts and should not have to maintain a declaration section.noMacroIfInOpenCodeis on, so%ifmust sit inside a%macrodefinition. Where a conditional has to stay in a service body, wrap the block in a small macro and call it unconditionally (%macro build_staged(); ... %end; %mend build_staged; %build_staged()), or resolve the value in a data step withcall symputx. Two traps: a%letinside such a wrapper is local, so a macro variable that must survive the block has to be written withcall symputx(...,'G'); and open-code%ifitself needs SAS 9.4M5, which the code no longer relies on.
No external assets
Data Controller must run entirely locally (offline / on-prem, no internet access). The built product must never fetch assets from remote servers — no external fonts, images, scripts, stylesheets, CDN links, or remote URLs in meta tags (e.g. og:image, og:url, itemprop="image"). All assets must be bundled and served locally.
The .agents folder
Agent-related content lives in .agents/: technical/agent-facing documentation goes in .agents/docs/ (not docs/), and skills in .agents/skills/. When writing explanatory or technical docs about the codebase, put them in .agents/docs/.
Skills installed via npx skills add are tracked in skills-lock.json and live under .agents/skills/. Repo-local custom skills (e.g. dc-sas for Data Controller SAS development, dc-estate-testing for estate deploys and test runs, handsontable, hyperformula) are kept here too. Avoid editing npx-managed skill files directly — use npx skills update instead.
Code comments and test names
Never reference items that are not active parts of the repository — no "the original bug", "regression from this fix", "this session/PR/commit", or similar ephemeral context. Comments and test names must be self-contained: describe the behaviour being asserted, not the history of how it was discovered. The one exception is a literal link to a ticket/issue tracker.