Context
#11306 AC2 originally prescribed adding an ESLint object-shorthand: ['error', 'always'] rule. PR #11926's V-B-A surfaced that Neo has no ESLint configuration — the project's lint mechanism is custom buildScripts/util/check-*.mjs scripts (check-whitespace.mjs, check-chore-sync.mjs), wired into lint-staged via package.json. The ESLint prescription was mis-shaped.
This ticket is the corrected-shape substrate gate prescribed during #11306 → #11926 review cycle (2026-05-24): mirror Neo's existing check-*.mjs convention for the shorthand gate.
The Problem
Without a mechanical gate, the key: key verbose form will silently re-accumulate after the #11306 sweep. Discipline-alone enforcement empirically fails — operator @tobiu's original #11306 observation (2026-05-13) flagged 71 instances had accumulated despite ES2015 shorthand being available since the project's inception. Same friction pattern (#11133's branch-discipline gap, #11116's commit-shape gap) demonstrates discipline-without-mechanical-gate produces predictable drift.
The Fix
Implement buildScripts/util/check-shorthand.mjs matching Neo's existing check-*.mjs convention:
- Scan
ai/, src/, test/playwright/ for *.mjs files (excluding worktrees, dist/, node_modules/)
- Regex
^(\s+)([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:\s*\2\s*[,}] — identifier-only (not \w+ which matches numeric keys; per @neo-gpt's #11926 review, the original regex falsely matched 0: 0 in ai/services/ConceptService.mjs)
- Exit non-zero if any matches found; print file:line of each violation
- Wire into
package.json lint-staged block alongside check-whitespace.mjs + check-chore-sync.mjs
Prescription
import {readFileSync} from 'fs';
import {execSync} from 'child_process';
const re = /^(\s+)([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:\s*\2\s*[,}]/;
const dirs = ['ai', 'src', 'test/playwright'];
const files = execSync(`find ${dirs.join(' ')} -name '*.mjs' -not -path '*/.claude/*' -not -path '*/dist/*' -not -path '*/node_modules/*'`).toString().trim().split('\n');
const violations = [];
for (const f of files) {
const lines = readFileSync(f, 'utf8').split('\n');
lines.forEach((line, i) => {
if (re.test(line)) violations.push(`${f}:${i + 1}: ${line.trim()}`);
});
}
if (violations.length > 0) {
console.error(`check-shorthand: ${violations.length} verbose key:key form(s) found:`);
violations.forEach(v => console.error(' ' + v));
console.error('\nUse ES2015 shorthand: {key} instead of {key: key}.');
process.exit(1);
}
console.log(`check-shorthand: ${files.length} files scanned, 0 violations.`);Acceptance Criteria
Out of Scope
- Migrating Neo to ESLint — that's a larger substrate decision, separate ticket
- Other lint surfaces (max-line-length, trailing whitespace beyond what
check-whitespace.mjs covers) — separate concerns
Empirical Anchors
Origin
Filed 2026-05-24 per @neo-gpt's #11926 review prescribing the corrected substrate shape for the deferred #11306 AC2 lint gate.
— @neo-opus-ada
Context
#11306 AC2 originally prescribed adding an ESLint
object-shorthand: ['error', 'always']rule. PR #11926's V-B-A surfaced that Neo has no ESLint configuration — the project's lint mechanism is custombuildScripts/util/check-*.mjsscripts (check-whitespace.mjs,check-chore-sync.mjs), wired intolint-stagedviapackage.json. The ESLint prescription was mis-shaped.This ticket is the corrected-shape substrate gate prescribed during #11306 → #11926 review cycle (2026-05-24): mirror Neo's existing
check-*.mjsconvention for the shorthand gate.The Problem
Without a mechanical gate, the
key: keyverbose form will silently re-accumulate after the #11306 sweep. Discipline-alone enforcement empirically fails — operator @tobiu's original #11306 observation (2026-05-13) flagged 71 instances had accumulated despite ES2015 shorthand being available since the project's inception. Same friction pattern (#11133's branch-discipline gap, #11116's commit-shape gap) demonstrates discipline-without-mechanical-gate produces predictable drift.The Fix
Implement
buildScripts/util/check-shorthand.mjsmatching Neo's existingcheck-*.mjsconvention:ai/,src/,test/playwright/for*.mjsfiles (excluding worktrees, dist/, node_modules/)^(\s+)([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:\s*\2\s*[,}]— identifier-only (not\w+which matches numeric keys; per @neo-gpt's #11926 review, the original regex falsely matched0: 0inai/services/ConceptService.mjs)package.jsonlint-stagedblock alongsidecheck-whitespace.mjs+check-chore-sync.mjsPrescription
// buildScripts/util/check-shorthand.mjs (skeleton) import {readFileSync} from 'fs'; import {execSync} from 'child_process'; const re = /^(\s+)([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:\s*\2\s*[,}]/; const dirs = ['ai', 'src', 'test/playwright']; const files = execSync(`find ${dirs.join(' ')} -name '*.mjs' -not -path '*/.claude/*' -not -path '*/dist/*' -not -path '*/node_modules/*'`).toString().trim().split('\n'); const violations = []; for (const f of files) { const lines = readFileSync(f, 'utf8').split('\n'); lines.forEach((line, i) => { if (re.test(line)) violations.push(`${f}:${i + 1}: ${line.trim()}`); }); } if (violations.length > 0) { console.error(`check-shorthand: ${violations.length} verbose key:key form(s) found:`); violations.forEach(v => console.error(' ' + v)); console.error('\nUse ES2015 shorthand: {key} instead of {key: key}.'); process.exit(1); } console.log(`check-shorthand: ${files.length} files scanned, 0 violations.`);Acceptance Criteria
buildScripts/util/check-shorthand.mjscreated with the regex + scan logic abovepackage.jsonlint-stagedblock extended to runcheck-shorthand.mjson*.mjsfilesnode buildScripts/util/check-shorthand.mjsexits 0 on currentdev(after #11306 / PR #11926 lands)Out of Scope
check-whitespace.mjscovers) — separate concernsEmpirical Anchors
buildScripts/util/check-whitespace.mjs— convention precedent for the file shapebuildScripts/util/check-chore-sync.mjs— second precedentOrigin
Filed 2026-05-24 per @neo-gpt's #11926 review prescribing the corrected substrate shape for the deferred #11306 AC2 lint gate.
— @neo-opus-ada