From 3a5ef5b63146691bb6713b9d21354139d037b012 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 18 Aug 2026 18:39:37 +0000 Subject: [PATCH 1/3] fix(cli): make init templates pass the author-time rules dev runs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `objectstack init my-app -t app --install` printed `Scaffold validated` and the next documented command, `npm run dev`, failed to compile: the shipped template declared no `sharingModel`, which the shipped `security-owd-unset` rule refuses (ADR-0090 D1 — absence is not a decision). The CLI's own template was rejected by the CLI's own rule set, and the developer on-ramp was dead. Two halves, both needed: 1. The `app` and `plugin` templates now author `sharingModel: 'private'` — the rule's own recommended default. A per-template sweep found `plugin` in the same state as the reported `app`; `empty` emits no objects and was clean. 2. `init`'s scaffold self-test now runs the author-time rule registry instead of only checking that the rendered config loads. It runs the `build` command's rule set — the same set `os dev` reaches by spawning `os compile` — so this is a shift-left, not a new bar: nothing that compiles today stops compiling, and a template that cannot compile fails at generation instead of at a user's first `dev`. The loader and the file emitter are now shared with the pin test (`validateScaffold`, `writeTemplateSrcFiles`), so the test drives the real command path rather than a copy that could drift from it. The pin sweeps `TEMPLATES` rather than asserting on `app`, so a template added later is covered the day it lands. Co-Authored-By: Claude --- packages/cli/src/commands/init.ts | 127 +++++++++++++---- packages/cli/src/utils/scaffold-validate.ts | 132 ++++++++++++++++++ .../init-scaffold-authoring-rules.test.ts | 126 +++++++++++++++++ 3 files changed, 355 insertions(+), 30 deletions(-) create mode 100644 packages/cli/src/utils/scaffold-validate.ts create mode 100644 packages/cli/test/init-scaffold-authoring-rules.test.ts diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index 90d35518da..953b6c6c62 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -6,7 +6,8 @@ import chalk from 'chalk'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; -import { printHeader, printSuccess, printError, printStep, printKV, printInfo } from '../utils/format.js'; +import { printHeader, printSuccess, printError, printStep, printKV, printInfo, formatZodErrors } from '../utils/format.js'; +import { validateScaffold } from '../utils/scaffold-validate.js'; // ─── Version resolution ────────────────────────────────────────────── // @@ -180,6 +181,12 @@ const ${toCamelCase(namespace)}Item: Data.Object = { defaultValue: 'draft', }, }, + // Org-wide default (OWD): who can see records they do NOT own. ADR-0090 D1 + // requires this to be an authored decision rather than an accident — the + // \`security-owd-unset\` author-time rule refuses an object without it, so a + // scaffold that omitted it could not compile. 'private' is the rule's own + // recommended default: owner + explicit shares. + sharingModel: 'private', }; export default ${toCamelCase(namespace)}Item; @@ -240,6 +247,12 @@ const ${toCamelCase(namespace)}Item: Data.Object = { required: true, }, }, + // Org-wide default (OWD): who can see records they do NOT own. ADR-0090 D1 + // requires this to be an authored decision rather than an accident — the + // \`security-owd-unset\` author-time rule refuses an object without it, so a + // scaffold that omitted it could not compile. 'private' is the rule's own + // recommended default: owner + explicit shares. + sharingModel: 'private', }; export default ${toCamelCase(namespace)}Item; @@ -296,6 +309,41 @@ function printWarning(msg: string) { console.log(chalk.yellow(` ⚠ ${msg}`)); } +/** + * Write a template's `srcFiles` into `targetDir` and return the relative paths + * written, in creation order. + * + * File paths use `__name__` as a placeholder for the NAMESPACE (not the npm + * name) so generated identifiers stay snake_case even when the project name + * contains hyphens (`my-app` → namespace `my_app` → `src/objects/my_app_item.ts`). + * + * Exported so the scaffold pin test generates projects through the real + * emitter instead of a copy of it. A test that re-implemented this loop could + * drift from it silently, and the drift would land in exactly the class the + * pin exists to catch: a shipped template the CLI's own rules refuse. + */ +export function writeTemplateSrcFiles( + srcFiles: Record string>, + targetDir: string, + projectName: string, + namespace: string, +): string[] { + const written: string[] = []; + for (const [filePath, contentFn] of Object.entries(srcFiles)) { + const resolvedPath = filePath.replace(/__name__/g, namespace); + const fullPath = path.join(targetDir, resolvedPath); + const dir = path.dirname(fullPath); + + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + + fs.writeFileSync(fullPath, contentFn(projectName, namespace)); + written.push(resolvedPath); + } + return written; +} + /** * Detect the package manager that invoked this CLI by inspecting * `npm_config_user_agent` (set by every modern PM). Falls back to `npm`, @@ -488,22 +536,9 @@ export default class Init extends Command { createdFiles.push('tsconfig.json'); } - // 4. Create src files. File paths use `__name__` as a placeholder for - // the namespace (NOT the npm name) so generated identifiers stay snake - // _case even when the project name contains hyphens (e.g. `my-app` → - // namespace `my_app` → `src/objects/my_app_item.ts`). - for (const [filePath, contentFn] of Object.entries(template.srcFiles)) { - const resolvedPath = filePath.replace(/__name__/g, namespace); - const fullPath = path.join(targetDir, resolvedPath); - const dir = path.dirname(fullPath); - - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); - } - - fs.writeFileSync(fullPath, contentFn(projectName, namespace)); - createdFiles.push(resolvedPath); - } + // 4. Create src files (see `writeTemplateSrcFiles` for the `__name__` + // placeholder rule and why the loop is exported). + createdFiles.push(...writeTemplateSrcFiles(template.srcFiles, targetDir, projectName, namespace)); // 5. Create .gitignore if missing const gitignorePath = path.join(targetDir, '.gitignore'); @@ -533,25 +568,57 @@ export default class Init extends Command { } } - // Self-test the scaffold so we catch template regressions (e.g. an - // invalid namespace or object name) before the user discovers them by - // running `objectstack dev`. Only runs when deps are present — - // `defineStack()` validation lives in `@objectstack/spec`. + // Self-test the scaffold so we catch template regressions before the + // user discovers them by running `objectstack dev`. Only runs when deps + // are present — `defineStack()` validation lives in `@objectstack/spec`. + // + // This used to check only that the rendered config LOADED and carried a + // `manifest.namespace`, which is how the CLI shipped a `-t app` template + // its own author-time rules refused: `init` printed `✓ Scaffold + // validated`, and the documented next command — `npm run dev` — died on + // `security-owd-unset` before the dev server ever started. The self-test + // now runs the same rule set `dev` reaches through `os compile` + // (`SCAFFOLD_RULE_COMMAND`), so a template that cannot compile fails + // HERE, at generation time, in CI, instead of at a user's first `dev`. + // It is a shift-left, not a new bar: same registry, same command tier. if (installSucceeded) { printStep('Validating scaffold...'); + let scaffoldRejected = false; try { - const { bundleRequire } = await import('bundle-require'); - const { mod } = await bundleRequire({ - filepath: path.join(targetDir, 'objectstack.config.ts'), - cwd: targetDir, - }); - const stack = mod.default ?? mod; - if (!stack?.manifest?.namespace) { - throw new Error('Rendered config has no manifest.namespace'); + const report = await validateScaffold(targetDir); + + for (const f of report.advisories.slice(0, 50)) { + printWarning(`${f.where}: ${f.message}`); + if (f.hint) console.log(chalk.dim(` ${f.hint}`)); + console.log(chalk.dim(` rule: ${f.rule} at ${f.path}`)); + } + + if (report.schemaError) { + printError('Scaffold validation failed: rendered config does not satisfy the protocol schema'); + formatZodErrors(report.schemaError); + scaffoldRejected = true; + } else if (report.errors.length > 0) { + // Report every failing rule at once, like `os validate` / `os build`. + printError( + `Scaffold validation failed: author-time rules rejected the generated project (${report.errors.length} issue${report.errors.length > 1 ? 's' : ''})`, + ); + for (const f of report.errors.slice(0, 50)) { + console.log(` • ${f.where}: ${f.message}`); + if (f.hint) console.log(chalk.dim(` ${f.hint}`)); + console.log(chalk.dim(` rule: ${f.rule} at ${f.path}`)); + } + scaffoldRejected = true; + } else { + printSuccess( + `Scaffold validated (namespace: ${report.namespace}; ${report.ruleCount} author-time rules passed)`, + ); } - printSuccess(`Scaffold validated (namespace: ${stack.manifest.namespace})`); } catch (err: any) { printError(`Scaffold validation failed: ${err.message || err}`); + scaffoldRejected = true; + } + + if (scaffoldRejected) { console.log(chalk.dim(' This is a CLI bug — please report it at https://github.com/objectstack-ai/objectstack/issues')); this.error('Scaffold validation failed'); } diff --git a/packages/cli/src/utils/scaffold-validate.ts b/packages/cli/src/utils/scaffold-validate.ts new file mode 100644 index 0000000000..e08fb84541 --- /dev/null +++ b/packages/cli/src/utils/scaffold-validate.ts @@ -0,0 +1,132 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Hold a freshly generated scaffold to the SAME author-time bar the user's + * very next command holds it to. + * + * ## The defect this exists to close + * + * `objectstack init my-app -t app --install` printed `✓ Scaffold validated` + * and the next command, `npm run dev`, failed to compile — the CLI's own + * shipped template was refused by the CLI's own shipped rule set + * (`security-owd-unset`: the template's object declared no `sharingModel`). + * `init`'s self-test only checked that the rendered config *loaded* and + * carried a `manifest.namespace`, so every author-time rule was unread at the + * one moment the CLI is generating the metadata itself. The documented + * on-ramp was dead and nothing in the CLI noticed. + * + * ## Why `'build'` and not `'validate'` + * + * `os dev` auto-compiles by spawning `os compile`, and `compile` runs + * `authoringRulesFor('build')`. Running the *same* command's rule set here is + * what keeps this a shift-left rather than a new, stricter gate: everything + * that survives `init` is exactly what survives the `dev` the user runs + * moments later. Picking a different command from the registry would let + * `init` refuse a scaffold `dev` accepts (or the reverse) — a second bar, the + * drift class `authoring-rules.ts` exists to prevent. + * + * The pipeline below mirrors `compile.ts` step-for-step for the same reason: + * normalize → lower callables → Zod parse → registry. A rule reading a + * differently-prepared stack is the same drift wearing a different hat. + */ + +import { join } from 'node:path'; +import { ObjectStackDefinitionSchema, normalizeStackInput } from '@objectstack/spec'; +import type { ZodError } from 'zod'; +import { + runAuthoringRules, + splitBySeverity, + authoringRulesFor, + type AuthoringCommand, + type AuthoringFinding, +} from '@objectstack/lint'; +import { lowerCallables } from './lower-callables.js'; +import { resolveSduiManifest } from './sdui-manifest.js'; + +/** + * The registry command whose rule set a generated scaffold is held to. + * + * Pinned to what `os dev` reaches through `os compile`. Exported so the pin + * test asserts the coupling instead of restating the string. + */ +export const SCAFFOLD_RULE_COMMAND: AuthoringCommand = 'build'; + +export interface ScaffoldRuleReport { + /** How many registry rules ran (for the progress line). */ + ruleCount: number; + /** Protocol-schema failure, if the stack did not parse at all. */ + schemaError: ZodError | null; + /** Gating findings — a non-empty list means `dev` would refuse this scaffold. */ + errors: AuthoringFinding[]; + /** `warning` / `info` findings — reported, never gating. */ + advisories: AuthoringFinding[]; +} + +/** + * Run the author-time rule set over an already-loaded stack config. + * + * Takes the config *object* rather than a path so the caller owns module + * loading (`init` bundle-requires the rendered config from the target dir, + * which is not `process.cwd()`), and so the pin test can drive real template + * output through the real rules without spawning a CLI. + */ +export function runScaffoldAuthoringRules(config: unknown): ScaffoldRuleReport { + const normalized = normalizeStackInput(config as Record); + const lowering = lowerCallables(normalized as Record); + const result = ObjectStackDefinitionSchema.safeParse(lowering.lowered); + + if (!result.success) { + return { + ruleCount: authoringRulesFor(SCAFFOLD_RULE_COMMAND).length, + schemaError: result.error as unknown as ZodError, + errors: [], + advisories: [], + }; + } + + const findings = runAuthoringRules(SCAFFOLD_RULE_COMMAND, { + normalized: normalized as Record, + parsed: result.data as Record, + sduiManifest: resolveSduiManifest(), + }); + const { errors, advisories } = splitBySeverity(findings); + + return { + ruleCount: authoringRulesFor(SCAFFOLD_RULE_COMMAND).length, + schemaError: null, + errors, + advisories, + }; +} + +/** + * Load a generated scaffold's `objectstack.config.ts` and run the author-time + * rule set over it. + * + * Module loading lives here — rather than in `init.ts` — so the pin test that + * sweeps every built-in template drives the SAME loader the command does. A + * test that re-implemented the load would be free to drift from it, and the + * drift would land precisely in the "the CLI's own template does not compile" + * class this whole file exists to close. + * + * The load is deliberately unchanged from what `init`'s self-test always did + * (no `external` list): only the checking after it is stronger. + * + * Note on `resolveSduiManifest()`: it reads `process.cwd()`, which for `init` + * is the directory the user invoked from, not `targetDir`. A freshly generated + * scaffold has no `sdui.manifest.json` either way, so both resolve to the copy + * shipped in `@objectstack/console` — the same input `os compile` gets when the + * user runs `dev` inside the new project. + */ +export async function validateScaffold(targetDir: string): Promise { + const { bundleRequire } = await import('bundle-require'); + const { mod } = await bundleRequire({ + filepath: join(targetDir, 'objectstack.config.ts'), + cwd: targetDir, + }); + const stack = mod.default ?? mod; + if (!stack?.manifest?.namespace) { + throw new Error('Rendered config has no manifest.namespace'); + } + return { namespace: String(stack.manifest.namespace), ...runScaffoldAuthoringRules(stack) }; +} diff --git a/packages/cli/test/init-scaffold-authoring-rules.test.ts b/packages/cli/test/init-scaffold-authoring-rules.test.ts new file mode 100644 index 0000000000..b455de9111 --- /dev/null +++ b/packages/cli/test/init-scaffold-authoring-rules.test.ts @@ -0,0 +1,126 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Every built-in `objectstack init` template must survive the author-time rule + * set the user's very next command runs. + * + * ## The incident this pins + * + * `npx @objectstack/cli init my-app -t app --install` printed `✓ Scaffold + * validated`, and `npm run dev` — the next line of the documented on-ramp — + * failed to compile: + * + * ✗ Author-time rules failed (1 issue) + * • object "my_app_item": custom object "my_app_item" declares no + * sharingModel (OWD)… rule: security-owd-unset + * + * The CLI's own shipped template was refused by the CLI's own shipped rules. + * Nothing caught it because `init`'s self-test only checked that the rendered + * config loaded, and no test ever ran a rule over generated template output. + * + * ## Why this is a per-template sweep and not one `app` assertion + * + * The defect class is "a shipped template the shipped rules refuse", so the + * pin iterates `TEMPLATES` — the same map `init` emits from. A template added + * later is swept the day it is added, without anyone remembering to extend + * this file. `app` was the reported instance; the sweep found `plugin` in the + * same state. + * + * The scaffold is generated through the command's own emitter + * (`writeTemplateSrcFiles`) and checked through the command's own self-test + * (`validateScaffold`), so neither half can drift from what `init` really does. + * + * Temp projects are created under this package's git-ignored `tmp/` (not + * `os.tmpdir()`) because the rendered config imports `@objectstack/spec`, + * which only resolves where Node can walk up into this package's + * `node_modules`. Keeping them out of `test/` also keeps generated `.ts` away + * from any glob that collects sources. + */ + +import { describe, it, expect, afterAll } from 'vitest'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { authoringRulesFor } from '@objectstack/lint'; +import { TEMPLATES, sanitizeNamespace, writeTemplateSrcFiles } from '../src/commands/init'; +import { validateScaffold, SCAFFOLD_RULE_COMMAND } from '../src/utils/scaffold-validate'; + +const HERE = path.dirname(fileURLToPath(import.meta.url)); +const TMP_ROOT = path.resolve(HERE, '../tmp'); +const PROJECT_NAME = 'my-app'; + +const roots: string[] = []; + +afterAll(() => { + for (const dir of roots) fs.rmSync(dir, { recursive: true, force: true }); +}); + +/** Generate one template into a throwaway directory, exactly as `init` does. */ +function generate(templateKey: string): string { + const template = TEMPLATES[templateKey]; + const namespace = sanitizeNamespace(PROJECT_NAME); + fs.mkdirSync(TMP_ROOT, { recursive: true }); + const root = fs.mkdtempSync(path.join(TMP_ROOT, `scaffold-${templateKey}-`)); + roots.push(root); + fs.writeFileSync( + path.join(root, 'objectstack.config.ts'), + template.configContent(PROJECT_NAME, namespace), + ); + writeTemplateSrcFiles(template.srcFiles, root, PROJECT_NAME, namespace); + return root; +} + +describe('init scaffolds pass the author-time rules `dev` runs', () => { + // `os dev` auto-compiles by spawning `os compile`, and `compile` runs the + // registry under the 'build' command. If `init` ran a DIFFERENT command's + // rule set it could refuse a scaffold `dev` accepts, or bless one `dev` + // refuses — the second-bar drift `authoring-rules.ts` exists to prevent. + it("holds scaffolds to the 'build' rule set, the one `dev` reaches via compile", () => { + expect(SCAFFOLD_RULE_COMMAND).toBe('build'); + expect(authoringRulesFor(SCAFFOLD_RULE_COMMAND).length).toBeGreaterThan(0); + }); + + it.each(Object.keys(TEMPLATES))( + 'template "%s" generates a project the author-time rules accept', + async (templateKey) => { + const report = await validateScaffold(generate(templateKey)); + + expect(report.schemaError, `template "${templateKey}" must satisfy the protocol schema`).toBeNull(); + + const rendered = report.errors + .map((f) => ` [${f.rule}] ${f.where} at ${f.path}: ${f.message}`) + .join('\n'); + expect( + report.errors, + `template "${templateKey}" generates a project its own author-time rules refuse.\n` + + `The user's next command (\`npm run dev\`) fails to compile on exactly these:\n${rendered}`, + ).toEqual([]); + + // A rule set that ran zero rules would satisfy the assertion above while + // checking nothing — the green-because-nothing-ran direction. + expect(report.ruleCount).toBeGreaterThan(0); + }, + 120_000, + ); + + // The reported instance, pinned by name so a future template edit that drops + // the field fails with the incident's own vocabulary rather than a bare count. + it.each( + Object.keys(TEMPLATES).filter((k) => Object.keys(TEMPLATES[k].srcFiles).length > 0), + )('template "%s" declares an authored OWD on every object it emits', (templateKey) => { + const namespace = sanitizeNamespace(PROJECT_NAME); + const objectSources = Object.entries(TEMPLATES[templateKey].srcFiles) + .filter(([filePath]) => filePath.replace(/__name__/g, namespace).includes('src/objects/') + && !filePath.endsWith('index.ts')) + .map(([, contentFn]) => contentFn(PROJECT_NAME, namespace)); + + expect(objectSources.length).toBeGreaterThan(0); + for (const src of objectSources) { + // ADR-0090 D1: absence is not a decision. 'private' is the rule's own + // recommended default (owner + explicit shares). + expect(src, `template "${templateKey}" object source must author sharingModel`).toMatch( + /sharingModel: '(private|public_read|public_read_write|controlled_by_parent)'/, + ); + } + }); +}); From 62bf55710717edc67ecc186bc2f9bfc9208bfee8 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 18 Aug 2026 18:42:15 +0000 Subject: [PATCH 2/3] chore(changeset): init scaffold OWD + author-time rule self-test Co-Authored-By: Claude --- ...init-scaffold-owd-and-author-time-rules.md | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .changeset/init-scaffold-owd-and-author-time-rules.md diff --git a/.changeset/init-scaffold-owd-and-author-time-rules.md b/.changeset/init-scaffold-owd-and-author-time-rules.md new file mode 100644 index 0000000000..5144f30c6c --- /dev/null +++ b/.changeset/init-scaffold-owd-and-author-time-rules.md @@ -0,0 +1,36 @@ +--- +"@objectstack/cli": patch +--- + +fix(cli): `objectstack init` scaffolds now compile — templates author an OWD, and the scaffold self-test runs the author-time rules (#9666) + +`objectstack init my-app -t app --install` reported `✓ Scaffold validated`, and +the next command in the documented on-ramp, `npm run dev`, failed to compile: + +``` +✗ Author-time rules failed (1 issue) +• object "my_app_item": custom object "my_app_item" declares no sharingModel (OWD)… + rule: security-owd-unset at objects[0].sharingModel +``` + +The CLI's own shipped template was refused by the CLI's own shipped rule set, so +the dev server never started on a freshly generated project. + +Two halves: + +- **Templates author an OWD.** The `app` and `plugin` templates now declare + `sharingModel: 'private'` on the object they emit — the rule's own recommended + default and the ADR-0090 D1 baseline (absence is not a decision). A sweep of + every built-in template found `plugin` in the same state as the reported `app`; + `empty` emits no objects and was already clean. +- **`init`'s self-test got teeth.** It used to check only that the rendered config + loaded and carried a `manifest.namespace`, which is why a template that could + not compile shipped. It now runs the author-time rule registry over the + generated project and refuses to report success when any rule rejects it. The + rule set is the `build` one — the same set `os dev` reaches by spawning + `os compile` — so this is a shift-left, not a stricter bar: nothing that + compiles today stops compiling, and a broken template now fails at generation + time instead of at a user's first `dev`. + +`✓ Scaffold validated` still prints, and now names how many author-time rules +passed. From 53e5b2080790a5f41e1d064274b7452a5e0eedb4 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 18 Aug 2026 19:10:22 +0000 Subject: [PATCH 3/3] fix(cli): spell the pin test's relative imports with .js (NodeNext) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The TEST_DEBT ratchet measured +4 raw tsc errors from the new test file: two TS2835 (relative imports need explicit extensions under moduleResolution NodeNext) and two more — TS7006 and TS18046 — that were downstream of those imports resolving to `any`. Fixing the two extensions cleared all four, so the @objectstack/cli entry sits back at its recorded 146 rather than being raised. Co-Authored-By: Claude --- packages/cli/test/init-scaffold-authoring-rules.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/test/init-scaffold-authoring-rules.test.ts b/packages/cli/test/init-scaffold-authoring-rules.test.ts index b455de9111..3bfb84e26d 100644 --- a/packages/cli/test/init-scaffold-authoring-rules.test.ts +++ b/packages/cli/test/init-scaffold-authoring-rules.test.ts @@ -42,8 +42,8 @@ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { authoringRulesFor } from '@objectstack/lint'; -import { TEMPLATES, sanitizeNamespace, writeTemplateSrcFiles } from '../src/commands/init'; -import { validateScaffold, SCAFFOLD_RULE_COMMAND } from '../src/utils/scaffold-validate'; +import { TEMPLATES, sanitizeNamespace, writeTemplateSrcFiles } from '../src/commands/init.js'; +import { validateScaffold, SCAFFOLD_RULE_COMMAND } from '../src/utils/scaffold-validate.js'; const HERE = path.dirname(fileURLToPath(import.meta.url)); const TMP_ROOT = path.resolve(HERE, '../tmp');