S2 EsLint implementation
All checks were successful
Build, test and publish / verify (push) Successful in 1m40s
All checks were successful
Build, test and publish / verify (push) Successful in 1m40s
This commit is contained in:
2
.husky/pre-commit
Normal file
2
.husky/pre-commit
Normal file
@@ -0,0 +1,2 @@
|
||||
cd agiledancers-fe || exit 1
|
||||
node node_modules/lint-staged/bin/lint-staged.js --concurrent false
|
||||
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["angular.ng-template", "dbaeumer.vscode-eslint"]
|
||||
}
|
||||
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"eslint.workingDirectories": ["./agiledancers-fe"]
|
||||
}
|
||||
47
README.md
47
README.md
@@ -169,6 +169,53 @@ backend on port 8080. Frontend builds and tests run independently of Maven:
|
||||
`npm run build` and `npm test`. See the frontend README for browser tests and
|
||||
production hosting requirements.
|
||||
|
||||
## Frontend lint and formatting
|
||||
|
||||
Run these commands from `agiledancers-fe/`:
|
||||
|
||||
```sh
|
||||
npm run lint
|
||||
npm run lint:fix
|
||||
```
|
||||
|
||||
`lint` checks maintained application code, inline and external Angular templates,
|
||||
unit tests, Playwright tests and JavaScript/TypeScript configuration. It uses the
|
||||
recommended JavaScript, Angular and type-aware TypeScript rules. It changes no
|
||||
files; both errors and warnings cause failure. Dependencies, generated output
|
||||
and `package-lock.json` are excluded. `lint:fix` applies ESLint's available fixes;
|
||||
review the diff and resolve any remaining findings manually. Prettier retains
|
||||
responsibility for formatting with the existing configuration.
|
||||
|
||||
Every branch pipeline runs `npm run lint` immediately after frontend `npm ci`,
|
||||
before frontend tests/builds and container E2E. Failure prevents image publication;
|
||||
findings are in the job log and retained `ci/artifacts/verify.log`. CI only checks
|
||||
and never fixes or stages source files.
|
||||
|
||||
Local `npm ci` automatically installs Husky at the repository root. Its commit
|
||||
hook runs **Prettier only**, through lint-staged with backup, rollback and hiding
|
||||
of unstaged changes in partially staged files enabled. It formats applicable
|
||||
staged frontend TypeScript, JavaScript, HTML, CSS and JSON files, then stages the
|
||||
formatting. Unrelated unstaged edits and files remain outside the commit. A
|
||||
formatter error or a conflict restoring unstaged edits aborts the commit and
|
||||
restores the original staged and unstaged state. Generated files are excluded;
|
||||
commits without applicable frontend changes skip these checks.
|
||||
|
||||
The complete Prettier-plus-ESLint hook exceeds the agreed three-second budget
|
||||
when all maintained files are staged. The selected Prettier-only hook meets the
|
||||
budget for single-file, mixed and all-file commits. ESLint is therefore run
|
||||
explicitly during development and always by CI. See [S2 validation](docs/s2-validation.md)
|
||||
for the measured environment, timings and preservation checks.
|
||||
|
||||
Hook installation is skipped when `CI` is set (including Docker builds) or
|
||||
`HUSKY=0`. Use `HUSKY=0 npm ci` to skip local installation, `HUSKY=0 git commit`
|
||||
to skip one hook execution, or `git config --local core.hooksPath /dev/null` to
|
||||
disable hooks for this clone. A subsequent normal `npm ci` enables them again.
|
||||
|
||||
VS Code recommends the ESLint extension in both root and frontend workspaces.
|
||||
With it installed, findings appear in the editor and Problems panel; the root
|
||||
workspace selects `agiledancers-fe/` as ESLint's working directory. No automatic
|
||||
fixing or formatting on save is required. `npm run lint` uses the same rules as CI.
|
||||
|
||||
## Gitea CI
|
||||
|
||||
Every branch push builds and tests both applications and their Docker images.
|
||||
|
||||
14
agiledancers-fe/.prettierignore
Normal file
14
agiledancers-fe/.prettierignore
Normal file
@@ -0,0 +1,14 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.angular/
|
||||
coverage/
|
||||
test-results/
|
||||
unit-results/
|
||||
playwright-report/
|
||||
package-lock.json
|
||||
out-tsc/
|
||||
bazel-out/
|
||||
tmp/
|
||||
typings/
|
||||
__screenshots__/
|
||||
.history/
|
||||
2
agiledancers-fe/.vscode/extensions.json
vendored
2
agiledancers-fe/.vscode/extensions.json
vendored
@@ -1,4 +1,4 @@
|
||||
{
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
|
||||
"recommendations": ["angular.ng-template"]
|
||||
"recommendations": ["angular.ng-template", "dbaeumer.vscode-eslint"]
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
FROM node:24.15.0-bookworm AS build
|
||||
WORKDIR /workspace
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
COPY tools/install-hooks.mjs tools/install-hooks.mjs
|
||||
RUN CI=true npm ci
|
||||
COPY . .
|
||||
RUN npm test && npm run build
|
||||
RUN npm run lint && npm test && npm run build
|
||||
|
||||
FROM nginx:1.28-alpine AS runtime
|
||||
ENV BACKEND_UPSTREAM=backend:8080
|
||||
|
||||
@@ -84,3 +84,9 @@ PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/path/to/chrome npm run e2e
|
||||
|
||||
Browser test screenshots and failure traces are written to the ignored
|
||||
`test-results` directory.
|
||||
|
||||
## Lint and commit formatting
|
||||
|
||||
See the [root README](../README.md#frontend-lint-and-formatting) for `npm run lint`,
|
||||
`npm run lint:fix`, editor support, CI enforcement and the measured Prettier-only
|
||||
commit hook, including installation and local opt-out.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { test, expect, Page } from '@playwright/test';
|
||||
import type { School } from '../src/app/core/api-models';
|
||||
|
||||
const backend = process.env['PLAYWRIGHT_BASE_URL'] || 'http://127.0.0.1:8080';
|
||||
let schoolToDelete: number | undefined;
|
||||
@@ -32,7 +33,6 @@ test('manages a school, shared memberships and shows through the real REST API',
|
||||
page,
|
||||
}) => {
|
||||
const schoolName = `E2E Studio ${Date.now()}`;
|
||||
let schoolId: number | undefined;
|
||||
const browserErrors: string[] = [];
|
||||
page.on('pageerror', (error) => browserErrors.push(error.message));
|
||||
await page.goto('/');
|
||||
@@ -43,8 +43,9 @@ test('manages a school, shared memberships and shows through the real REST API',
|
||||
response.url().endsWith('/api/dance-schools') && response.request().method() === 'POST',
|
||||
);
|
||||
await save(page);
|
||||
schoolId = (await (await created).json()).id;
|
||||
schoolToDelete = schoolId;
|
||||
const school: unknown = await (await created).json();
|
||||
expect(school).toEqual(expect.objectContaining({ id: expect.any(Number) }));
|
||||
schoolToDelete = (school as School).id;
|
||||
await page.getByRole('link', { name: new RegExp(schoolName) }).click();
|
||||
await expect(page.getByRole('heading', { name: schoolName, exact: true })).toBeVisible();
|
||||
|
||||
|
||||
47
agiledancers-fe/eslint.config.mjs
Normal file
47
agiledancers-fe/eslint.config.mjs
Normal file
@@ -0,0 +1,47 @@
|
||||
import js from '@eslint/js';
|
||||
import { defineConfig, globalIgnores } from 'eslint/config';
|
||||
import tseslint from 'typescript-eslint';
|
||||
import angular from 'angular-eslint';
|
||||
import prettier from 'eslint-config-prettier';
|
||||
import globals from 'globals';
|
||||
|
||||
export default defineConfig(
|
||||
globalIgnores([
|
||||
'**/node_modules/**',
|
||||
'**/dist/**',
|
||||
'**/out-tsc/**',
|
||||
'**/bazel-out/**',
|
||||
'**/tmp/**',
|
||||
'**/typings/**',
|
||||
'**/__screenshots__/**',
|
||||
'**/.history/**',
|
||||
'**/.angular/**',
|
||||
'**/coverage/**',
|
||||
'**/test-results/**',
|
||||
'**/unit-results/**',
|
||||
'**/playwright-report/**',
|
||||
'package-lock.json',
|
||||
]),
|
||||
{
|
||||
files: ['**/*.{js,mjs,cjs}'],
|
||||
extends: [js.configs.recommended],
|
||||
languageOptions: { globals: globals.node },
|
||||
},
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
tseslint.configs.recommendedTypeChecked,
|
||||
angular.configs.tsRecommended,
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: './tsconfig.eslint.json',
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
processor: angular.processInlineTemplates,
|
||||
},
|
||||
{ files: ['**/*.html'], extends: [angular.configs.templateRecommended] },
|
||||
prettier,
|
||||
);
|
||||
21
agiledancers-fe/lint-staged.config.mjs
Normal file
21
agiledancers-fe/lint-staged.config.mjs
Normal file
@@ -0,0 +1,21 @@
|
||||
import path from 'node:path';
|
||||
|
||||
export default {
|
||||
'*.{ts,js,mjs,cjs,html,css,json,prettierrc}': (files) => {
|
||||
const maintained = files.filter((file) => {
|
||||
const relative = path
|
||||
.relative(import.meta.dirname, file)
|
||||
.split(path.sep)
|
||||
.join('/');
|
||||
return (
|
||||
!relative.startsWith('../') &&
|
||||
!/^(node_modules|dist|out-tsc|bazel-out|tmp|typings|__screenshots__|\.history|\.angular|coverage|test-results|unit-results|playwright-report)\//.test(
|
||||
relative,
|
||||
) &&
|
||||
relative !== 'package-lock.json'
|
||||
);
|
||||
});
|
||||
if (!maintained.length) return [];
|
||||
return [`prettier --write ${maintained.map((file) => JSON.stringify(file)).join(' ')}`];
|
||||
},
|
||||
};
|
||||
1481
agiledancers-fe/package-lock.json
generated
1481
agiledancers-fe/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,10 @@
|
||||
"build": "ng build",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test --watch=false",
|
||||
"e2e": "playwright test"
|
||||
"e2e": "playwright test",
|
||||
"lint": "eslint . --max-warnings 0",
|
||||
"lint:fix": "eslint . --fix --max-warnings 0",
|
||||
"prepare": "node tools/install-hooks.mjs"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
@@ -24,10 +27,19 @@
|
||||
"@angular/build": "22.2.4",
|
||||
"@angular/cli": "22.2.4",
|
||||
"@angular/compiler-cli": "22.2.3",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@playwright/test": "1.64.1",
|
||||
"@types/node": "22.20.5",
|
||||
"angular-eslint": "22.6.0",
|
||||
"eslint": "10.13.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"globals": "17.13.0",
|
||||
"husky": "9.1.7",
|
||||
"jsdom": "30.1.3",
|
||||
"lint-staged": "17.6.0",
|
||||
"prettier": "3.9.10",
|
||||
"typescript": "6.0.3",
|
||||
"typescript-eslint": "8.72.1",
|
||||
"vitest": "5.0.5"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -16,7 +16,10 @@ describe('School JSON import', () => {
|
||||
afterEach(() => http.verify());
|
||||
async function select() {
|
||||
const selected = component.select({
|
||||
target: { files: [{ text: async () => '{"formatVersion":1}' }], value: 'file.json' },
|
||||
target: {
|
||||
files: [{ text: () => Promise.resolve('{"formatVersion":1}') }],
|
||||
value: 'file.json',
|
||||
},
|
||||
} as unknown as Event);
|
||||
await Promise.resolve();
|
||||
return { selected, request: http.expectOne('/api/dance-schools/import/preview') };
|
||||
|
||||
245
agiledancers-fe/tools/check-hook.mjs
Normal file
245
agiledancers-fe/tools/check-hook.mjs
Normal file
@@ -0,0 +1,245 @@
|
||||
/** Isolated hook acceptance checks and full-process timing; never stages this checkout. */
|
||||
import assert from 'node:assert/strict';
|
||||
import { execFileSync, spawnSync } from 'node:child_process';
|
||||
import { cpSync, mkdtempSync, readFileSync, writeFileSync, symlinkSync } from 'node:fs';
|
||||
import { tmpdir, cpus, platform, release } from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { performance } from 'node:perf_hooks';
|
||||
|
||||
const source = fileURLToPath(new URL('../../', import.meta.url));
|
||||
const root = mkdtempSync(path.join(tmpdir(), 's2-hook-acceptance-'));
|
||||
const frontend = path.join(root, 'agiledancers-fe');
|
||||
const excluded = new Set([
|
||||
'node_modules',
|
||||
'dist',
|
||||
'.angular',
|
||||
'coverage',
|
||||
'test-results',
|
||||
'unit-results',
|
||||
'playwright-report',
|
||||
'_',
|
||||
]);
|
||||
cpSync(path.join(source, 'agiledancers-fe'), frontend, {
|
||||
recursive: true,
|
||||
filter: (file) => !excluded.has(path.basename(file)),
|
||||
});
|
||||
cpSync(path.join(source, '.husky'), path.join(root, '.husky'), {
|
||||
recursive: true,
|
||||
filter: (file) => !excluded.has(path.basename(file)),
|
||||
});
|
||||
symlinkSync(
|
||||
path.join(source, 'agiledancers-fe/node_modules'),
|
||||
path.join(frontend, 'node_modules'),
|
||||
'dir',
|
||||
);
|
||||
const env = { ...process.env };
|
||||
delete env.CI;
|
||||
delete env.HUSKY;
|
||||
function run(command, args, cwd = root, environment = env) {
|
||||
return execFileSync(command, args, {
|
||||
cwd,
|
||||
env: environment,
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
}).trimEnd();
|
||||
}
|
||||
const git = (...args) => run('git', args);
|
||||
const write = (file, content) => writeFileSync(path.join(root, file), content);
|
||||
const read = (file) => readFileSync(path.join(root, file), 'utf8');
|
||||
const fixture = 'agiledancers-fe/src/hook-fixture.ts';
|
||||
const baseline =
|
||||
'export const hookValue = { value: 1 };\n\n// Independent edits below.\n\nexport const otherValue = 1;\n';
|
||||
write(fixture, baseline);
|
||||
write('unrelated.txt', 'baseline\n');
|
||||
git('init', '-q');
|
||||
git('config', 'user.name', 'S2 validation');
|
||||
git('config', 'user.email', 's2@example.invalid');
|
||||
git('add', '.');
|
||||
git('-c', 'core.hooksPath=/dev/null', 'commit', '-qm', 'Fixture baseline');
|
||||
git('config', 'core.hooksPath', '/dev/null');
|
||||
run('node', ['tools/install-hooks.mjs'], frontend, { ...env, CI: 'true' });
|
||||
assert.equal(git('config', '--get', 'core.hooksPath'), '/dev/null');
|
||||
run('node', ['tools/install-hooks.mjs'], frontend, { ...env, HUSKY: '0' });
|
||||
assert.equal(git('config', '--get', 'core.hooksPath'), '/dev/null');
|
||||
console.log('PASS CI and HUSKY=0 skip hook installation');
|
||||
run('node', ['tools/install-hooks.mjs'], frontend);
|
||||
assert.equal(git('config', '--get', 'core.hooksPath'), '.husky/_');
|
||||
const reset = () => git('reset', '--hard', '-q', 'HEAD');
|
||||
const snapshot = () =>
|
||||
JSON.stringify([
|
||||
git('diff', '--cached', '--binary'),
|
||||
git('diff', '--binary'),
|
||||
git('status', '--porcelain'),
|
||||
git('stash', 'list'),
|
||||
]);
|
||||
function hook() {
|
||||
const start = performance.now();
|
||||
const result = spawnSync('sh', ['.husky/_/pre-commit'], { cwd: root, env, encoding: 'utf8' });
|
||||
return {
|
||||
seconds: Number(((performance.now() - start) / 1000).toFixed(3)),
|
||||
exit: result.status,
|
||||
output: result.stdout + result.stderr,
|
||||
};
|
||||
}
|
||||
function passed(name) {
|
||||
console.log(`PASS ${name}`);
|
||||
}
|
||||
|
||||
// Format only the index version while independently staged/unstaged edits survive.
|
||||
write(fixture, baseline.replace('{ value: 1 }', '{value:2}'));
|
||||
git('add', fixture);
|
||||
write(fixture, read(fixture).replace('otherValue = 1', 'otherValue = 2'));
|
||||
write('unrelated.txt', 'staged\n');
|
||||
git('add', 'unrelated.txt');
|
||||
write('unrelated.txt', 'staged\nunstaged\n');
|
||||
const result = hook();
|
||||
assert.equal(result.exit, 0, result.output);
|
||||
assert.match(git('show', `:${fixture}`), /hookValue = \{ value: 2 \}/);
|
||||
assert.match(git('show', `:${fixture}`), /otherValue = 1/);
|
||||
assert.match(read(fixture), /otherValue = 2/);
|
||||
assert.equal(git('show', ':unrelated.txt'), 'staged');
|
||||
assert.equal(read('unrelated.txt'), 'staged\nunstaged\n');
|
||||
assert.equal(git('stash', 'list'), '');
|
||||
passed('partially staged formatting preserves unrelated staged and unstaged changes');
|
||||
reset();
|
||||
|
||||
write('unrelated.txt', 'only backend/documentation\n');
|
||||
git('add', 'unrelated.txt');
|
||||
let before = snapshot();
|
||||
assert.equal(hook().exit, 0);
|
||||
assert.equal(snapshot(), before);
|
||||
passed('commits without frontend changes leave state unchanged');
|
||||
reset();
|
||||
|
||||
write('agiledancers-fe/package-lock.json', read('agiledancers-fe/package-lock.json') + '\n\n');
|
||||
git('add', 'agiledancers-fe/package-lock.json');
|
||||
before = snapshot();
|
||||
assert.equal(hook().exit, 0);
|
||||
assert.equal(snapshot(), before);
|
||||
passed('generated lockfile is excluded');
|
||||
reset();
|
||||
|
||||
write(
|
||||
'agiledancers-fe/src/app/app.ts',
|
||||
read('agiledancers-fe/src/app/app.ts') + '\nexport const formatterCheck={value:2};\n',
|
||||
);
|
||||
write(fixture, 'export const broken = ;\n');
|
||||
git('add', 'agiledancers-fe/src/app/app.ts', fixture);
|
||||
write('unrelated.txt', 'unstaged survives failures\n');
|
||||
before = snapshot();
|
||||
const failed = hook();
|
||||
assert.notEqual(failed.exit, 0);
|
||||
assert.match(failed.output, /SyntaxError|Unexpected token/);
|
||||
assert.equal(snapshot(), before);
|
||||
passed('formatter failure restores index, worktree and stash');
|
||||
reset();
|
||||
|
||||
write(fixture, baseline.replace('{ value: 1 }', '{value:2}'));
|
||||
git('add', fixture);
|
||||
write(fixture, read(fixture).replace('{value:2}', '{value:3}'));
|
||||
before = snapshot();
|
||||
const conflict = hook();
|
||||
assert.notEqual(conflict.exit, 0, conflict.output);
|
||||
assert.match(conflict.output, /conflict|Failed to restore unstaged/i);
|
||||
assert.equal(snapshot(), before);
|
||||
passed('unstaged reconciliation conflict restores original state');
|
||||
reset();
|
||||
|
||||
// Benchmark the rejected combined candidate using the same hook and state protections.
|
||||
const selectedConfig = read('agiledancers-fe/lint-staged.config.mjs');
|
||||
write(
|
||||
'agiledancers-fe/lint-staged.combined.mjs',
|
||||
`import base from './lint-staged.config.mjs';
|
||||
export default Object.fromEntries(Object.entries(base).map(([pattern, task]) => [pattern, files => {
|
||||
const commands = task(files);
|
||||
const lintable = files.filter(file => /\\.(ts|js|mjs|cjs|html)$/.test(file));
|
||||
return commands.length && lintable.length
|
||||
? [...commands, 'eslint --max-warnings 0 ' + lintable.map(file => JSON.stringify(file)).join(' ')] : commands;
|
||||
}]));\n`,
|
||||
);
|
||||
const selectedHook = read('.husky/pre-commit');
|
||||
write(
|
||||
'.husky/pre-commit',
|
||||
selectedHook.replace(
|
||||
'--concurrent false',
|
||||
'--concurrent false --config lint-staged.combined.mjs',
|
||||
),
|
||||
);
|
||||
write(fixture, baseline.replace('{ value: 1 }', '{value:2}'));
|
||||
write(
|
||||
'agiledancers-fe/src/app/app.ts',
|
||||
read('agiledancers-fe/src/app/app.ts') + '\nexport const badLint: any = 1;\n',
|
||||
);
|
||||
git('add', fixture, 'agiledancers-fe/src/app/app.ts');
|
||||
before = snapshot();
|
||||
const lintFailure = hook();
|
||||
assert.notEqual(lintFailure.exit, 0);
|
||||
assert.match(lintFailure.output, /no-explicit-any/);
|
||||
assert.equal(snapshot(), before);
|
||||
passed('combined candidate restores formatting after ESLint failure');
|
||||
reset();
|
||||
|
||||
const all = git('ls-files', 'agiledancers-fe')
|
||||
.split('\n')
|
||||
.filter((file) => !file.endsWith('/package-lock.json'));
|
||||
const scopes = {
|
||||
single: ['agiledancers-fe/src/app/app.ts'],
|
||||
mixed: [
|
||||
'agiledancers-fe/src/app/app.ts',
|
||||
'agiledancers-fe/src/app/app.html',
|
||||
'agiledancers-fe/src/app/app.css',
|
||||
'agiledancers-fe/angular.json',
|
||||
'agiledancers-fe/eslint.config.mjs',
|
||||
'agiledancers-fe/e2e/school-management.spec.ts',
|
||||
],
|
||||
all,
|
||||
};
|
||||
const timings = [];
|
||||
for (const mode of ['combined', 'prettier-only']) {
|
||||
for (const [scope, files] of Object.entries(scopes)) {
|
||||
for (let repeat = 1; repeat <= 3; repeat++) {
|
||||
reset();
|
||||
write(
|
||||
'.husky/pre-commit',
|
||||
mode === 'combined'
|
||||
? selectedHook.replace(
|
||||
'--concurrent false',
|
||||
'--concurrent false --config lint-staged.combined.mjs',
|
||||
)
|
||||
: selectedHook,
|
||||
);
|
||||
for (const file of files) {
|
||||
const addition = /\.(ts|js|mjs|cjs)$/.test(file)
|
||||
? '\n// Hook timing fixture.\n'
|
||||
: file.endsWith('.html')
|
||||
? '\n<!-- Hook timing fixture. -->\n'
|
||||
: file.endsWith('.css')
|
||||
? '\n/* Hook timing fixture. */\n'
|
||||
: '\n';
|
||||
write(file, read(file) + addition);
|
||||
}
|
||||
git('add', '--', ...files);
|
||||
const measurement = hook();
|
||||
assert.equal(measurement.exit, 0, measurement.output);
|
||||
timings.push({ mode, scope, repeat, files, seconds: measurement.seconds });
|
||||
console.log(`TIMING ${mode} ${scope} ${repeat}: ${measurement.seconds}s`);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.equal(read('agiledancers-fe/lint-staged.config.mjs').includes(selectedConfig.trim()), true);
|
||||
const report = {
|
||||
machine: cpus()[0].model,
|
||||
system: `${platform()} ${release()}`,
|
||||
node: process.version,
|
||||
npm: run('npm', ['--version']),
|
||||
root,
|
||||
timings,
|
||||
};
|
||||
writeFileSync(path.join(root, 'report.json'), JSON.stringify(report, null, 2) + '\n');
|
||||
console.log(`Evidence: ${path.join(root, 'report.json')}`);
|
||||
const selected = timings.filter((result) => result.mode === 'prettier-only');
|
||||
assert.ok(
|
||||
selected.every((result) => result.seconds <= 3),
|
||||
'Prettier-only exceeds the three-second budget; do not enable the hook.',
|
||||
);
|
||||
13
agiledancers-fe/tools/install-hooks.mjs
Normal file
13
agiledancers-fe/tools/install-hooks.mjs
Normal file
@@ -0,0 +1,13 @@
|
||||
import { existsSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import husky from 'husky';
|
||||
|
||||
// Docker uses CI=true; CI and local opt-out must never change Git configuration.
|
||||
if (!process.env.CI && process.env.HUSKY !== '0') {
|
||||
const root = fileURLToPath(new URL('../../', import.meta.url));
|
||||
if (existsSync(new URL('../../.git', import.meta.url))) {
|
||||
process.chdir(root);
|
||||
const error = husky('.husky');
|
||||
if (error) throw new Error(error);
|
||||
}
|
||||
}
|
||||
14
agiledancers-fe/tsconfig.eslint.json
Normal file
14
agiledancers-fe/tsconfig.eslint.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": { "types": ["vitest/globals", "node"], "noEmit": true },
|
||||
"include": ["src/**/*.ts", "e2e/**/*.ts", "*.ts"],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
".angular",
|
||||
"coverage",
|
||||
"test-results",
|
||||
"unit-results",
|
||||
"playwright-report"
|
||||
]
|
||||
}
|
||||
@@ -102,6 +102,21 @@ class CiTests {
|
||||
equal(0L,f.calls("build")); equal(0L,f.calls("push"));
|
||||
}
|
||||
});
|
||||
test("Frontend lint runs after install and blocks tests and build on failure", () -> {
|
||||
try (Fixture f = new Fixture()) {
|
||||
f.fake.lintFailure = true;
|
||||
try { f.pipeline.verifyFrontend(); throw new AssertionError("Expected failure"); }
|
||||
catch (Commands.Failure error) { equal(1,error.exitCode); }
|
||||
equal(List.of(List.of("npm", "ci"), List.of("npm", "run", "lint")), f.fake.calls);
|
||||
}
|
||||
});
|
||||
test("Frontend verification checks before testing and building", () -> {
|
||||
try (Fixture f = new Fixture()) {
|
||||
f.pipeline.verifyFrontend();
|
||||
equal(List.of("ci", "run lint", "test -- --reporters=default --reporters=junit --output-file=unit-results/junit.xml", "run build"),
|
||||
f.fake.calls.stream().map(a -> String.join(" ", a.subList(1,a.size()))).toList());
|
||||
}
|
||||
});
|
||||
test("Process execution enforces its deadline", () -> {
|
||||
Path dir = Files.createTempDirectory("ci-process-test-");
|
||||
try {
|
||||
@@ -135,13 +150,14 @@ class CiTests {
|
||||
final List<List<String>> calls = new ArrayList<>();
|
||||
final Map<String,String> images = new HashMap<>(), tags = new HashMap<>();
|
||||
String head = SHA, pushFailure = "", pullFailure = "";
|
||||
boolean manifestError, mavenFailure;
|
||||
boolean manifestError, mavenFailure, lintFailure;
|
||||
boolean dockerAvailable = true;
|
||||
FakeCommands() { super(Duration.ofMinutes(1)); }
|
||||
@Override Result execute(List<String> args, Path cwd, Map<String,String> env, String input, boolean echo) {
|
||||
calls.add(List.copyOf(args));
|
||||
if (args.getFirst().equals("git")) return new Result(0,SHA);
|
||||
if (args.getFirst().equals("mvn") && mavenFailure) return new Result(42,"deliberate failure");
|
||||
if (args.equals(List.of("npm", "run", "lint")) && lintFailure) return new Result(1,"deliberate lint failure");
|
||||
if (!args.getFirst().equals("docker")) return new Result(0,"");
|
||||
String command = args.get(1), ref = args.getLast();
|
||||
if (command.equals("info")) return new Result(dockerAvailable ? 0 : 1,args.contains("{{.Architecture}}") ? "amd64" : "linux");
|
||||
|
||||
@@ -69,10 +69,7 @@ class Pipeline {
|
||||
run("java", "-version"); run("node", "--version"); run("mvn", "-B", "-ntp", "clean", "verify");
|
||||
CiTests.run();
|
||||
Path frontend = root.resolve("agiledancers-fe");
|
||||
commands.execute(List.of("npm", "ci"), frontend, env, null, true).checked();
|
||||
commands.execute(List.of("npm", "test", "--", "--reporters=default", "--reporters=junit",
|
||||
"--output-file=unit-results/junit.xml"), frontend, env, null, true).checked();
|
||||
commands.execute(List.of("npm", "run", "build"), frontend, env, null, true).checked();
|
||||
verifyFrontend();
|
||||
String platform = env.get("TARGET_PLATFORM");
|
||||
run("docker", "build", "--platform", platform, "--target", "prebuilt", "--label",
|
||||
"org.opencontainers.image.revision=" + commit, "-t", backendImage, ".");
|
||||
@@ -101,6 +98,14 @@ class Pipeline {
|
||||
}
|
||||
}
|
||||
}
|
||||
void verifyFrontend() throws Exception {
|
||||
Path frontend = root.resolve("agiledancers-fe");
|
||||
commands.execute(List.of("npm", "ci"), frontend, env, null, true).checked();
|
||||
commands.execute(List.of("npm", "run", "lint"), frontend, env, null, true).checked();
|
||||
commands.execute(List.of("npm", "test", "--", "--reporters=default", "--reporters=junit",
|
||||
"--output-file=unit-results/junit.xml"), frontend, env, null, true).checked();
|
||||
commands.execute(List.of("npm", "run", "build"), frontend, env, null, true).checked();
|
||||
}
|
||||
void waitForApplication() throws Exception {
|
||||
for (int i = 0; i < 60; i++) {
|
||||
if (result(false,"docker", "exec", runName + "-frontend", "wget", "-q", "-O", "-",
|
||||
|
||||
184
docs/s2-hook-timings.json
Normal file
184
docs/s2-hook-timings.json
Normal file
@@ -0,0 +1,184 @@
|
||||
{
|
||||
"date": "2026-10-29",
|
||||
"machine": "AMD Ryzen 7 7730U with Radeon Graphics",
|
||||
"system": "linux 7.0.0-38-generic",
|
||||
"node": "v22.22.3",
|
||||
"npm": "10.9.8",
|
||||
"scopes": {
|
||||
"single": [
|
||||
"agiledancers-fe/src/app/app.ts"
|
||||
],
|
||||
"mixed": [
|
||||
"agiledancers-fe/src/app/app.ts",
|
||||
"agiledancers-fe/src/app/app.html",
|
||||
"agiledancers-fe/src/app/app.css",
|
||||
"agiledancers-fe/angular.json",
|
||||
"agiledancers-fe/eslint.config.mjs",
|
||||
"agiledancers-fe/e2e/school-management.spec.ts"
|
||||
],
|
||||
"all": [
|
||||
"agiledancers-fe/.dockerignore",
|
||||
"agiledancers-fe/.editorconfig",
|
||||
"agiledancers-fe/.gitignore",
|
||||
"agiledancers-fe/.nvmrc",
|
||||
"agiledancers-fe/.prettierignore",
|
||||
"agiledancers-fe/.prettierrc",
|
||||
"agiledancers-fe/.vscode/extensions.json",
|
||||
"agiledancers-fe/.vscode/launch.json",
|
||||
"agiledancers-fe/.vscode/tasks.json",
|
||||
"agiledancers-fe/Dockerfile",
|
||||
"agiledancers-fe/README.md",
|
||||
"agiledancers-fe/angular.json",
|
||||
"agiledancers-fe/e2e/school-management.spec.ts",
|
||||
"agiledancers-fe/eslint.config.mjs",
|
||||
"agiledancers-fe/lint-staged.config.mjs",
|
||||
"agiledancers-fe/nginx/default.conf.template",
|
||||
"agiledancers-fe/package.json",
|
||||
"agiledancers-fe/playwright.config.ts",
|
||||
"agiledancers-fe/proxy.conf.json",
|
||||
"agiledancers-fe/public/favicon.svg",
|
||||
"agiledancers-fe/src/app/app.config.ts",
|
||||
"agiledancers-fe/src/app/app.css",
|
||||
"agiledancers-fe/src/app/app.html",
|
||||
"agiledancers-fe/src/app/app.routes.ts",
|
||||
"agiledancers-fe/src/app/app.spec.ts",
|
||||
"agiledancers-fe/src/app/app.ts",
|
||||
"agiledancers-fe/src/app/core/api-error.ts",
|
||||
"agiledancers-fe/src/app/core/api-models.ts",
|
||||
"agiledancers-fe/src/app/core/school-api.spec.ts",
|
||||
"agiledancers-fe/src/app/core/school-api.ts",
|
||||
"agiledancers-fe/src/app/schools/school-page.html",
|
||||
"agiledancers-fe/src/app/schools/school-page.ts",
|
||||
"agiledancers-fe/src/app/schools/schools-page.html",
|
||||
"agiledancers-fe/src/app/schools/schools-page.ts",
|
||||
"agiledancers-fe/src/app/shared/confirm-dialog.ts",
|
||||
"agiledancers-fe/src/app/shared/dialog-frame.ts",
|
||||
"agiledancers-fe/src/app/shared/entity-editor.spec.ts",
|
||||
"agiledancers-fe/src/app/shared/entity-editor.ts",
|
||||
"agiledancers-fe/src/app/shared/group-members.spec.ts",
|
||||
"agiledancers-fe/src/app/shared/group-members.ts",
|
||||
"agiledancers-fe/src/app/shared/school-transfer.spec.ts",
|
||||
"agiledancers-fe/src/app/shared/school-transfer.ts",
|
||||
"agiledancers-fe/src/hook-fixture.ts",
|
||||
"agiledancers-fe/src/index.html",
|
||||
"agiledancers-fe/src/main.ts",
|
||||
"agiledancers-fe/src/styles.css",
|
||||
"agiledancers-fe/tools/check-hook.mjs",
|
||||
"agiledancers-fe/tools/install-hooks.mjs",
|
||||
"agiledancers-fe/tsconfig.app.json",
|
||||
"agiledancers-fe/tsconfig.eslint.json",
|
||||
"agiledancers-fe/tsconfig.json",
|
||||
"agiledancers-fe/tsconfig.spec.json"
|
||||
]
|
||||
},
|
||||
"timings": [
|
||||
{
|
||||
"mode": "combined",
|
||||
"scope": "single",
|
||||
"repeat": 1,
|
||||
"seconds": 2.875
|
||||
},
|
||||
{
|
||||
"mode": "combined",
|
||||
"scope": "single",
|
||||
"repeat": 2,
|
||||
"seconds": 2.776
|
||||
},
|
||||
{
|
||||
"mode": "combined",
|
||||
"scope": "single",
|
||||
"repeat": 3,
|
||||
"seconds": 2.894
|
||||
},
|
||||
{
|
||||
"mode": "combined",
|
||||
"scope": "mixed",
|
||||
"repeat": 1,
|
||||
"seconds": 3.779
|
||||
},
|
||||
{
|
||||
"mode": "combined",
|
||||
"scope": "mixed",
|
||||
"repeat": 2,
|
||||
"seconds": 4.099
|
||||
},
|
||||
{
|
||||
"mode": "combined",
|
||||
"scope": "mixed",
|
||||
"repeat": 3,
|
||||
"seconds": 4.478
|
||||
},
|
||||
{
|
||||
"mode": "combined",
|
||||
"scope": "all",
|
||||
"repeat": 1,
|
||||
"seconds": 5.916
|
||||
},
|
||||
{
|
||||
"mode": "combined",
|
||||
"scope": "all",
|
||||
"repeat": 2,
|
||||
"seconds": 6.137
|
||||
},
|
||||
{
|
||||
"mode": "combined",
|
||||
"scope": "all",
|
||||
"repeat": 3,
|
||||
"seconds": 4.728
|
||||
},
|
||||
{
|
||||
"mode": "prettier-only",
|
||||
"scope": "single",
|
||||
"repeat": 1,
|
||||
"seconds": 0.313
|
||||
},
|
||||
{
|
||||
"mode": "prettier-only",
|
||||
"scope": "single",
|
||||
"repeat": 2,
|
||||
"seconds": 0.338
|
||||
},
|
||||
{
|
||||
"mode": "prettier-only",
|
||||
"scope": "single",
|
||||
"repeat": 3,
|
||||
"seconds": 0.401
|
||||
},
|
||||
{
|
||||
"mode": "prettier-only",
|
||||
"scope": "mixed",
|
||||
"repeat": 1,
|
||||
"seconds": 0.648
|
||||
},
|
||||
{
|
||||
"mode": "prettier-only",
|
||||
"scope": "mixed",
|
||||
"repeat": 2,
|
||||
"seconds": 0.72
|
||||
},
|
||||
{
|
||||
"mode": "prettier-only",
|
||||
"scope": "mixed",
|
||||
"repeat": 3,
|
||||
"seconds": 0.647
|
||||
},
|
||||
{
|
||||
"mode": "prettier-only",
|
||||
"scope": "all",
|
||||
"repeat": 1,
|
||||
"seconds": 1.573
|
||||
},
|
||||
{
|
||||
"mode": "prettier-only",
|
||||
"scope": "all",
|
||||
"repeat": 2,
|
||||
"seconds": 1.441
|
||||
},
|
||||
{
|
||||
"mode": "prettier-only",
|
||||
"scope": "all",
|
||||
"repeat": 3,
|
||||
"seconds": 1.276
|
||||
}
|
||||
]
|
||||
}
|
||||
126
docs/s2-validation.md
Normal file
126
docs/s2-validation.md
Normal file
@@ -0,0 +1,126 @@
|
||||
# S2 implementation evidence
|
||||
|
||||
Validated on the development mini-PC on 2026-10-29: AMD Ryzen 7 7730U (8 cores,
|
||||
16 threads), Ubuntu 26.04.1 LTS, Linux 7.0.0-38-generic x86_64, approximately
|
||||
27 GiB RAM, Node v22.22.3, npm 10.9.8. The standalone Docker build uses the
|
||||
existing Node 24.15.0 image.
|
||||
|
||||
## Lint scope and rules
|
||||
|
||||
ESLint 10.13.0, typescript-eslint 8.72.1 and angular-eslint 22.6.0 support the
|
||||
existing Angular 22 and TypeScript 6.0.3 versions. Framework versions are unchanged.
|
||||
The flat configuration applies recommended JavaScript, recommended type-checked
|
||||
TypeScript and recommended Angular rules, plus the recommended template rules
|
||||
for both inline and external templates. No rule suppressions were needed.
|
||||
`eslint-config-prettier` prevents conflicts with the existing formatting settings.
|
||||
|
||||
`tsconfig.eslint.json` supplies one explicit type-checking project covering
|
||||
application files, unit tests, Playwright tests and root TypeScript configuration.
|
||||
This includes E2E and Playwright configuration outside Angular's app/test
|
||||
compilation projects, without changing the application compilation scope.
|
||||
Node types resolve `process.env` in those files. The existing test findings were
|
||||
fixed by treating Playwright response JSON as unknown before checking its shape,
|
||||
and returning an explicit resolved promise from the fake file reader.
|
||||
|
||||
Injected probes confirmed `no-floating-promises` for application, unit-test,
|
||||
Playwright and configuration TypeScript; `no-unused-vars` for JavaScript;
|
||||
and `template/no-negated-async` in both external and inline Angular templates.
|
||||
Generated files are excluded. A warning-only CLI probe returned exit status 1
|
||||
with `--max-warnings 0`.
|
||||
|
||||
## Complete hook measurements
|
||||
|
||||
Run from the repository root, with dependencies installed:
|
||||
|
||||
```sh
|
||||
node agiledancers-fe/tools/check-hook.mjs
|
||||
```
|
||||
|
||||
This creates an isolated temporary Git repository from the current frontend and
|
||||
hook sources, links the installed dependency directory, makes an initial fixture
|
||||
commit and installs the real Husky wrapper. The real checkout and its index are
|
||||
not staged or committed by the checks. The temporary repository is retained and
|
||||
its report path is printed so failures can be inspected.
|
||||
|
||||
Each timed invocation runs `sh .husky/_/pre-commit` in a new process. Timing
|
||||
starts before Husky and includes file selection, lint-staged backup, hiding
|
||||
unstaged edits, fresh tool startup, formatting/checking, restaging and cleanup.
|
||||
No ESLint or Prettier cache is enabled. The final measurement ran while the
|
||||
standalone Docker build was also running; all selected-hook runs still passed. Three fresh executions per scenario and
|
||||
candidate are recorded; there is no runtime timeout interrupting writers.
|
||||
|
||||
The combined candidate uses the same hook, adding type-aware ESLint after
|
||||
Prettier. The selected configuration uses only Prettier. Staged scopes are:
|
||||
|
||||
- Single: `src/app/app.ts`.
|
||||
- Mixed: application TS, HTML and CSS, `angular.json`, `eslint.config.mjs` and
|
||||
the Playwright spec (six files).
|
||||
- All: all 52 maintained frontend files, including non-applicable files that the
|
||||
hook must skip, plus a small TS preservation-test fixture; generated lockfile
|
||||
excluded. Exact paths and raw measurements are in
|
||||
[the timing report](s2-hook-timings.json).
|
||||
|
||||
| Candidate | Staged scope | Run 1 | Run 2 | Run 3 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| combined | single | 2.875 s | 2.776 s | 2.894 s |
|
||||
| combined | mixed | 3.779 s | 4.099 s | 4.478 s |
|
||||
| combined | all | 5.916 s | 6.137 s | 4.728 s |
|
||||
| prettier-only | single | 0.313 s | 0.338 s | 0.401 s |
|
||||
| prettier-only | mixed | 0.648 s | 0.720 s | 0.647 s |
|
||||
| prettier-only | all | 1.573 s | 1.441 s | 1.276 s |
|
||||
|
||||
The combined candidate exceeds three seconds, so it is rejected. Every measured
|
||||
Prettier-only execution is below three seconds, so that hook is installed. These
|
||||
are measurements on this mini-PC, not a guarantee for other machines or future
|
||||
repository sizes. Rerun the checks when the tooling or scope changes.
|
||||
|
||||
## Preservation and installation checks
|
||||
|
||||
The isolated checks assert all of the following against the real hook, comparing
|
||||
both cached and unstaged binary diffs, status and stash state where appropriate:
|
||||
|
||||
- Partially staged formatting updates only the intended index version and
|
||||
retains independent unstaged changes in the same file.
|
||||
- An unrelated file with both staged and unstaged edits remains intact.
|
||||
- A commit without frontend changes leaves the state unchanged.
|
||||
- `package-lock.json` is not formatted or restaged.
|
||||
- A formatter syntax error after another file has been formatted aborts and
|
||||
restores the original index and worktree.
|
||||
- A conflict restoring an unstaged change on the formatted line aborts and
|
||||
restores the original index and worktree.
|
||||
- The rejected combined candidate also restores all changes when ESLint fails
|
||||
after successful formatting.
|
||||
- No backup stash remains after successful recovery.
|
||||
- `CI=true` and `HUSKY=0` leave an existing hooks configuration untouched;
|
||||
normal local installation selects `.husky/_`.
|
||||
|
||||
A real local `npm ci` successfully installed `.husky/_` in this checkout.
|
||||
Docker sets `CI=true` for installation and copies the prepare entry point before
|
||||
`npm ci`, so the lifecycle script can safely skip hook installation. The
|
||||
standalone build runs lint, tests and production compilation after copying sources.
|
||||
|
||||
## Build and pipeline verification
|
||||
|
||||
The full Java-based pipeline was executed locally without invoking publication:
|
||||
|
||||
```sh
|
||||
JAVA_HOME=/usr/lib/jvm/java-25-openjdk-amd64 \
|
||||
PATH=/usr/lib/jvm/java-25-openjdk-amd64/bin:/home/wanja-krah/.nvm/versions/node/v22.22.3/bin:/usr/local/bin:/usr/bin:/bin \
|
||||
TARGET_PLATFORM=linux/amd64 CI_RUN_ID=s2-local CI_RUN_ATTEMPT=2 \
|
||||
java ci/Ci.java verify
|
||||
```
|
||||
|
||||
Results: 20 backend tests, 17 CI-helper tests, clean frontend lint, 14 frontend
|
||||
unit tests, production build, both application images and both container E2E
|
||||
tests passed. Diagnostics remain in the ignored `ci/artifacts/` directory.
|
||||
A Gitea workflow run for these uncommitted changes has not been executed.
|
||||
|
||||
The CI-helper regression checks assert that lint immediately follows `npm ci`
|
||||
and precedes tests/build; a lint failure stops frontend verification with its
|
||||
nonzero status. Existing failed-verification and publication guards ensure only
|
||||
a completely verified commit can publish. The shared command runner writes lint
|
||||
output to the job log and retained `verify.log`. CI never invokes `lint:fix`.
|
||||
|
||||
`npm run lint:fix` followed by `npm run lint` also passed without further changes.
|
||||
The standalone Docker build was checked separately with an isolated Docker
|
||||
configuration to avoid the desktop credential helper; no images were published.
|
||||
@@ -1,6 +1,6 @@
|
||||
# S2 — ESLint for the frontend
|
||||
|
||||
Status: REFINED
|
||||
Status: IN_REVIEW
|
||||
|
||||
Agreed after the grill-me review; shared understanding confirmed.
|
||||
|
||||
@@ -183,36 +183,43 @@ consistent. Record the performance evidence with the implementation documentatio
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] Compatible tooling checks maintained application TypeScript/JavaScript,
|
||||
- [x] Compatible tooling checks maintained application TypeScript/JavaScript,
|
||||
Angular templates, unit tests, Playwright tests and applicable frontend
|
||||
configuration using the agreed recommended and type-aware rules.
|
||||
- [ ] All existing violations are fixed; there is no legacy baseline allowance.
|
||||
- [x] All existing violations are fixed; there is no legacy baseline allowance.
|
||||
Frontend builds, unit tests and container end-to-end tests still pass.
|
||||
- [ ] `npm run lint` checks the full scope without modifications and fails on
|
||||
- [x] `npm run lint` checks the full scope without modifications and fails on
|
||||
either errors or warnings.
|
||||
- [ ] `npm run lint:fix` offers explicit local autofixes; CI does not modify source.
|
||||
- [ ] Formatting remains under Prettier, without conflicting ESLint rules.
|
||||
- [ ] Any necessary rule suppressions are narrow and concretely justified.
|
||||
- [ ] Every branch pipeline runs lint immediately after frontend `npm ci` and
|
||||
- [x] `npm run lint:fix` offers explicit local autofixes; CI does not modify source.
|
||||
- [x] Formatting remains under Prettier, without conflicting ESLint rules.
|
||||
- [x] Any necessary rule suppressions are narrow and concretely justified.
|
||||
- [x] Every branch pipeline runs lint immediately after frontend `npm ci` and
|
||||
before frontend tests/builds and container E2E; lint failure prevents
|
||||
publication and leaves actionable diagnostic output.
|
||||
- [ ] VS Code recommends ESLint without mandatory fix/format-on-save settings.
|
||||
- [ ] Complete-hook measurements cover the agreed single-file, mixed-file and
|
||||
- [x] VS Code recommends ESLint without mandatory fix/format-on-save settings.
|
||||
- [x] Complete-hook measurements cover the agreed single-file, mixed-file and
|
||||
all-maintained-files cases with fresh processes and no tool cache.
|
||||
- [ ] The measured three-second budget determines combined tools, Prettier-only
|
||||
- [x] The measured three-second budget determines combined tools, Prettier-only
|
||||
or no active hook; no writing formatter is forcibly interrupted to meet it.
|
||||
- [ ] If installed, the hook processes only applicable staged frontend files,
|
||||
- [x] If installed, the hook processes only applicable staged frontend files,
|
||||
applies Prettier formatting before any ESLint check, and preserves unrelated
|
||||
files and unstaged edits, including partially staged files.
|
||||
- [ ] A failed installed hook restores the original staged and unstaged state and
|
||||
- [x] A failed installed hook restores the original staged and unstaged state and
|
||||
blocks the commit with a concrete explanation.
|
||||
- [ ] Hook installation is automatic for local `npm ci` when applicable, skipped
|
||||
- [x] Hook installation is automatic for local `npm ci` when applicable, skipped
|
||||
in CI/Docker, and has documented local opt-out instructions.
|
||||
- [ ] Root `README.md` explains `npm run lint`, `npm run lint:fix`, CI enforcement
|
||||
- [x] Root `README.md` explains `npm run lint`, `npm run lint:fix`, CI enforcement
|
||||
and the selected hook behavior; measurement evidence is included.
|
||||
|
||||
## Implementation handoff
|
||||
|
||||
This specification records agreed behavior. It does not install ESLint, change
|
||||
application code, modify the pipeline, install a hook or update command usage in
|
||||
the README yet. Those changes and the timing measurements are implementation work.
|
||||
The agreed behavior has been implemented and is ready for review. See the
|
||||
implementation evidence below.
|
||||
|
||||
## Implementation evidence
|
||||
|
||||
Implemented with ESLint flat configuration, type-aware TypeScript and Angular
|
||||
template checks. The measured three-second budget selects a Prettier-only
|
||||
Husky/lint-staged hook; ESLint remains mandatory in the Java-based pipeline.
|
||||
See [validation and timings](../../docs/s2-validation.md) for scope, preservation
|
||||
checks, local installation and successful builds/unit/container E2E tests.
|
||||
Reference in New Issue
Block a user