Install eslint@9 + typescript-eslint@8 + globals@16 + eslint-plugin-n@17 + eslint-plugin-no-only-tests@3 + typescript as devDependencies. eslint.config.mjs (flat config): - Global ignores: node_modules, dist, .worktrees, .claude, coverage, the 12 generated get-shit-done/bin/lib/*.cjs files - Block for get-shit-done/bin/**/*.cjs + scripts/**/*.cjs: js.recommended + eslint-plugin-n + local plugin; generic quality rules (no-var, prefer-const, no-unused-vars, no-empty, n/no-process-exit) - Block for tests/**/*.test.cjs: no-only-tests (error), local timing rules, no-restricted-syntax timing bans eslint-rules/ local plugin (three AST rules, all at warn pending cleanup): - no-source-grep: flag readFileSync on source .cjs/.js/.ts + text methods - no-magic-sleep-in-tests: flag Atomics.wait and await-new-Promise(setTimeout) - no-elapsed-assertion: flag assert*() on timing props (elapsed/duration/took/ms) tsconfig.lint.json: allowJs + checkJs + noEmit for future type-aware passes. tests/eslint-rules.test.cjs: 15 RuleTester unit tests (all pass, 0 fail). package.json: add lint/lint:fix scripts; remove lint:tests (subsumed by ESLint local/no-source-grep). Rules that produced pre-existing errors downgraded to warn: no-useless-escape, no-unsafe-finally, no-regex-spaces, no-control-regex, no-irregular-whitespace. ESLint exits 0 (warnings ok). .github/workflows/test.yml lint-tests job: add npm ci + ESLint step; remove "Lint — no source-grep tests" step (now covered by ESLint); bump timeout 3→5 min. .gitignore: add node_modules/.cache/eslint/ entry. eslint --fix auto-cleaned: no-regex-spaces in tests, prefer-const in state.cjs, redundant eslint-disable-next-line comments. Co-authored-by: CI Rebase Check <ci@gsd-redux> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
8
.github/workflows/test.yml
vendored
8
.github/workflows/test.yml
vendored
@@ -84,7 +84,7 @@ jobs:
|
|||||||
# Runs once, not per matrix lane.
|
# Runs once, not per matrix lane.
|
||||||
lint-tests:
|
lint-tests:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 3
|
timeout-minutes: 5
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
@@ -94,10 +94,12 @@ jobs:
|
|||||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
|
- name: Install dev dependencies
|
||||||
|
run: npm ci --ignore-scripts
|
||||||
|
- name: Lint — ESLint (source-grep + timing + no-only-tests + quality)
|
||||||
|
run: npx eslint . --cache --cache-location node_modules/.cache/eslint/
|
||||||
- name: Lint — skill dependency graph
|
- name: Lint — skill dependency graph
|
||||||
run: npm run lint:skill-deps
|
run: npm run lint:skill-deps
|
||||||
- name: Lint — no source-grep tests
|
|
||||||
run: node scripts/lint-no-source-grep.cjs
|
|
||||||
- name: Lint — test file count per module
|
- name: Lint — test file count per module
|
||||||
run: node scripts/lint-test-file-count.cjs
|
run: node scripts/lint-test-file-count.cjs
|
||||||
- name: Lint — command contract (ADR-0002)
|
- name: Lint — command contract (ADR-0002)
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,5 +1,8 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# ESLint cache
|
||||||
|
node_modules/.cache/eslint/
|
||||||
TO-DOS.md
|
TO-DOS.md
|
||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
/research.claude/
|
/research.claude/
|
||||||
|
|||||||
103
eslint-rules/no-elapsed-assertion.cjs
Normal file
103
eslint-rules/no-elapsed-assertion.cjs
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* no-elapsed-assertion
|
||||||
|
*
|
||||||
|
* Flag assert*() calls whose argument reads a property named
|
||||||
|
* /^(elapsed|duration|took|ms)$/ or compares such an identifier.
|
||||||
|
* Timing assertions are flaky and should not be in the test suite.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @type {import('eslint').Rule.RuleModule} */
|
||||||
|
const rule = {
|
||||||
|
meta: {
|
||||||
|
type: 'problem',
|
||||||
|
docs: {
|
||||||
|
description:
|
||||||
|
'Disallow timing assertions (elapsed, duration, took, ms) in assert calls',
|
||||||
|
category: 'Best Practices',
|
||||||
|
},
|
||||||
|
schema: [],
|
||||||
|
messages: {
|
||||||
|
noElapsedAssertion:
|
||||||
|
'Timing assertion detected: assert*() on a timing property (elapsed/duration/took/ms). Timing assertions are flaky — assert on observable behavior instead.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
create(context) {
|
||||||
|
const TIMING_PROPS = /^(elapsed|duration|took|ms)$/;
|
||||||
|
|
||||||
|
function containsTimingRef(node) {
|
||||||
|
if (!node) return false;
|
||||||
|
|
||||||
|
// foo.elapsed, foo.duration, foo.took, foo.ms
|
||||||
|
if (
|
||||||
|
node.type === 'MemberExpression' &&
|
||||||
|
node.property.type === 'Identifier' &&
|
||||||
|
TIMING_PROPS.test(node.property.name)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Identifier directly: elapsed, duration, took, ms
|
||||||
|
if (node.type === 'Identifier' && TIMING_PROPS.test(node.name)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Binary expression: elapsed > 100, duration <= 500, etc.
|
||||||
|
if (node.type === 'BinaryExpression') {
|
||||||
|
return containsTimingRef(node.left) || containsTimingRef(node.right);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logical expression: elapsed && elapsed > 0
|
||||||
|
if (node.type === 'LogicalExpression') {
|
||||||
|
return containsTimingRef(node.left) || containsTimingRef(node.right);
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnaryExpression: !elapsed
|
||||||
|
if (node.type === 'UnaryExpression') {
|
||||||
|
return containsTimingRef(node.argument);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAssertCall(node) {
|
||||||
|
if (node.callee.type === 'Identifier') {
|
||||||
|
return /^assert/.test(node.callee.name);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
node.callee.type === 'MemberExpression' &&
|
||||||
|
node.callee.object.type === 'Identifier' &&
|
||||||
|
node.callee.object.name === 'assert'
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// assert.strict.* or assert/strict
|
||||||
|
if (
|
||||||
|
node.callee.type === 'MemberExpression' &&
|
||||||
|
node.callee.object.type === 'MemberExpression' &&
|
||||||
|
node.callee.object.object.type === 'Identifier' &&
|
||||||
|
node.callee.object.object.name === 'assert'
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
CallExpression(node) {
|
||||||
|
if (!isAssertCall(node)) return;
|
||||||
|
|
||||||
|
// Check all arguments for timing refs
|
||||||
|
for (const arg of node.arguments) {
|
||||||
|
if (containsTimingRef(arg)) {
|
||||||
|
context.report({ node, messageId: 'noElapsedAssertion' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = rule;
|
||||||
80
eslint-rules/no-magic-sleep-in-tests.cjs
Normal file
80
eslint-rules/no-magic-sleep-in-tests.cjs
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* no-magic-sleep-in-tests
|
||||||
|
*
|
||||||
|
* In *.test.cjs files, flag:
|
||||||
|
* - Atomics.wait used as a sleep
|
||||||
|
* - raw setTimeout used for synchronization (i.e., awaited or used without a callback driven by logic)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @type {import('eslint').Rule.RuleModule} */
|
||||||
|
const rule = {
|
||||||
|
meta: {
|
||||||
|
type: 'problem',
|
||||||
|
docs: {
|
||||||
|
description:
|
||||||
|
'Disallow Atomics.wait sleeps and raw setTimeout synchronization in test files',
|
||||||
|
category: 'Best Practices',
|
||||||
|
},
|
||||||
|
schema: [],
|
||||||
|
messages: {
|
||||||
|
atomicsWaitSleep:
|
||||||
|
'Atomics.wait() used as a sleep in tests. Use a proper async wait pattern instead.',
|
||||||
|
setTimeoutSync:
|
||||||
|
'Raw setTimeout used for synchronization in tests. Use proper async patterns (promises, events, polling) instead.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
create(context) {
|
||||||
|
const filename = context.getFilename();
|
||||||
|
|
||||||
|
// Only applies in test files
|
||||||
|
if (!filename.endsWith('.test.cjs')) return {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
CallExpression(node) {
|
||||||
|
// Atomics.wait(...)
|
||||||
|
if (
|
||||||
|
node.callee.type === 'MemberExpression' &&
|
||||||
|
node.callee.object.type === 'Identifier' &&
|
||||||
|
node.callee.object.name === 'Atomics' &&
|
||||||
|
node.callee.property.name === 'wait'
|
||||||
|
) {
|
||||||
|
context.report({ node, messageId: 'atomicsWaitSleep' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// setTimeout(...) used for synchronization:
|
||||||
|
// - await new Promise(resolve => setTimeout(resolve, N))
|
||||||
|
// - setTimeout(() => resolve(...), N) pattern
|
||||||
|
// - setTimeout(cb, N) where N is a numeric literal (magic delay)
|
||||||
|
if (
|
||||||
|
node.callee.type === 'Identifier' &&
|
||||||
|
node.callee.name === 'setTimeout'
|
||||||
|
) {
|
||||||
|
const args = node.arguments;
|
||||||
|
// setTimeout(something, numericLiteral) — magic delay
|
||||||
|
if (
|
||||||
|
args.length >= 2 &&
|
||||||
|
args[1].type === 'Literal' &&
|
||||||
|
typeof args[1].value === 'number'
|
||||||
|
) {
|
||||||
|
// Check if it's in an await expression or a Promise constructor
|
||||||
|
const parent = node.parent;
|
||||||
|
const grandParent = parent && parent.parent;
|
||||||
|
const isInPromise =
|
||||||
|
(grandParent &&
|
||||||
|
grandParent.type === 'NewExpression' &&
|
||||||
|
grandParent.callee.type === 'Identifier' &&
|
||||||
|
grandParent.callee.name === 'Promise') ||
|
||||||
|
(parent && parent.type === 'AwaitExpression');
|
||||||
|
if (isInPromise) {
|
||||||
|
context.report({ node, messageId: 'setTimeoutSync' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = rule;
|
||||||
116
eslint-rules/no-source-grep.cjs
Normal file
116
eslint-rules/no-source-grep.cjs
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* no-source-grep
|
||||||
|
*
|
||||||
|
* Flags variables bound to readFileSync() of a .cjs/.js/.ts source path that
|
||||||
|
* later have .includes/.match/.startsWith/.indexOf called on them.
|
||||||
|
*
|
||||||
|
* Honor file-level escape comment: // allow-test-rule: <reason>
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @type {import('eslint').Rule.RuleModule} */
|
||||||
|
const rule = {
|
||||||
|
meta: {
|
||||||
|
type: 'problem',
|
||||||
|
docs: {
|
||||||
|
description:
|
||||||
|
'Disallow reading source .cjs/.js/.ts files with readFileSync and then doing text search on the result',
|
||||||
|
category: 'Best Practices',
|
||||||
|
},
|
||||||
|
schema: [],
|
||||||
|
messages: {
|
||||||
|
noSourceGrep:
|
||||||
|
'Source-grep test: do not read source .cjs/.js/.ts files with readFileSync and call .includes/.match/.startsWith/.indexOf on the result. Use require() to run the module instead. Add // allow-test-rule: <reason> at the top of the file to suppress.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
create(context) {
|
||||||
|
const sourceCode = context.getSourceCode
|
||||||
|
? context.getSourceCode()
|
||||||
|
: context.sourceCode;
|
||||||
|
|
||||||
|
// Check for file-level escape comment
|
||||||
|
const comments = sourceCode.getAllComments();
|
||||||
|
const hasAllowAnnotation = comments.some(
|
||||||
|
(c) => /allow-test-rule:\s*\S/.test(c.value)
|
||||||
|
);
|
||||||
|
if (hasAllowAnnotation) return {};
|
||||||
|
|
||||||
|
// Track variable names bound to readFileSync of a source path
|
||||||
|
const sourceGrepVars = new Set();
|
||||||
|
|
||||||
|
// Detect if a node represents a readFileSync call on a source file (.cjs/.js/.ts)
|
||||||
|
// that lives in a source directory (bin, lib, get-shit-done, src).
|
||||||
|
function isSourceReadFileSync(node) {
|
||||||
|
if (node.type !== 'CallExpression') return false;
|
||||||
|
|
||||||
|
// Match: readFileSync(...) or fs.readFileSync(...) or require('fs').readFileSync(...)
|
||||||
|
const callee = node.callee;
|
||||||
|
const isFsRead =
|
||||||
|
(callee.type === 'Identifier' && callee.name === 'readFileSync') ||
|
||||||
|
(callee.type === 'MemberExpression' &&
|
||||||
|
callee.property.type === 'Identifier' &&
|
||||||
|
callee.property.name === 'readFileSync');
|
||||||
|
|
||||||
|
if (!isFsRead) return false;
|
||||||
|
|
||||||
|
const args = node.arguments;
|
||||||
|
if (!args || args.length === 0) return false;
|
||||||
|
|
||||||
|
const firstArg = args[0];
|
||||||
|
const fullSrc = sourceCode.getText(firstArg);
|
||||||
|
|
||||||
|
return looksLikeSourcePath(fullSrc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Given the source text of a path expression, determine if it references
|
||||||
|
// a .cjs/.js/.ts source file in a source directory.
|
||||||
|
function looksLikeSourcePath(src) {
|
||||||
|
// Must end with a .cjs, .js, or .ts extension (in a string)
|
||||||
|
const hasCjsExt = /['"`.][^'"`.]*\.(?:cjs|js|ts)['"`)]/i.test(src);
|
||||||
|
if (!hasCjsExt) return false;
|
||||||
|
|
||||||
|
// Must reference a source directory indicator somewhere in the expression
|
||||||
|
const hasSourceDir = /['"](?:bin|lib|get-shit-done|src)['"]/i.test(src);
|
||||||
|
return hasSourceDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TEXT_METHODS = new Set(['includes', 'match', 'startsWith', 'endsWith', 'indexOf', 'search']);
|
||||||
|
|
||||||
|
return {
|
||||||
|
VariableDeclarator(node) {
|
||||||
|
// const varName = readFileSync(...) OR const varName = fs.readFileSync(...)
|
||||||
|
if (node.init && isSourceReadFileSync(node.init)) {
|
||||||
|
if (node.id.type === 'Identifier') {
|
||||||
|
sourceGrepVars.add(node.id.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
AssignmentExpression(node) {
|
||||||
|
if (node.right && isSourceReadFileSync(node.right)) {
|
||||||
|
if (node.left.type === 'Identifier') {
|
||||||
|
sourceGrepVars.add(node.left.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
CallExpression(node) {
|
||||||
|
// varName.includes(...), varName.match(...), etc.
|
||||||
|
if (
|
||||||
|
node.callee.type === 'MemberExpression' &&
|
||||||
|
TEXT_METHODS.has(node.callee.property.name)
|
||||||
|
) {
|
||||||
|
const obj = node.callee.object;
|
||||||
|
if (obj.type === 'Identifier' && sourceGrepVars.has(obj.name)) {
|
||||||
|
context.report({ node, messageId: 'noSourceGrep' });
|
||||||
|
}
|
||||||
|
// Inline: readFileSync(...).includes(...)
|
||||||
|
if (isSourceReadFileSync(obj)) {
|
||||||
|
context.report({ node, messageId: 'noSourceGrep' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = rule;
|
||||||
138
eslint.config.mjs
Normal file
138
eslint.config.mjs
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import js from '@eslint/js';
|
||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
import globals from 'globals';
|
||||||
|
import pluginN from 'eslint-plugin-n';
|
||||||
|
import noOnlyTests from 'eslint-plugin-no-only-tests';
|
||||||
|
import { existsSync } from 'fs';
|
||||||
|
import { join, dirname } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
// Local plugin with three custom AST rules
|
||||||
|
import noSourceGrep from './eslint-rules/no-source-grep.cjs';
|
||||||
|
import noMagicSleepInTests from './eslint-rules/no-magic-sleep-in-tests.cjs';
|
||||||
|
import noElapsedAssertion from './eslint-rules/no-elapsed-assertion.cjs';
|
||||||
|
|
||||||
|
const localPlugin = {
|
||||||
|
rules: {
|
||||||
|
'no-source-grep': noSourceGrep,
|
||||||
|
'no-magic-sleep-in-tests': noMagicSleepInTests,
|
||||||
|
'no-elapsed-assertion': noElapsedAssertion,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generated bin/lib files — never lint
|
||||||
|
const GENERATED_CJS_IGNORES = [
|
||||||
|
'get-shit-done/bin/lib/command-aliases.cjs',
|
||||||
|
'get-shit-done/bin/lib/configuration.cjs',
|
||||||
|
'get-shit-done/bin/lib/decisions.cjs',
|
||||||
|
'get-shit-done/bin/lib/phase-lifecycle.cjs',
|
||||||
|
'get-shit-done/bin/lib/plan-scan.cjs',
|
||||||
|
'get-shit-done/bin/lib/project-root.cjs',
|
||||||
|
'get-shit-done/bin/lib/schema-detect.cjs',
|
||||||
|
'get-shit-done/bin/lib/secrets.cjs',
|
||||||
|
'get-shit-done/bin/lib/state-document.cjs',
|
||||||
|
'get-shit-done/bin/lib/validate.cjs',
|
||||||
|
'get-shit-done/bin/lib/workstream-inventory-builder.cjs',
|
||||||
|
'get-shit-done/bin/lib/workstream-name-policy.cjs',
|
||||||
|
];
|
||||||
|
|
||||||
|
const sdkSrcExists = existsSync(join(__dirname, 'sdk', 'src'));
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
// ── Global ignores ─────────────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
ignores: [
|
||||||
|
'node_modules/**',
|
||||||
|
'**/dist/**',
|
||||||
|
'sdk/dist/**',
|
||||||
|
'.worktrees/**',
|
||||||
|
'.claude/**',
|
||||||
|
'coverage/**',
|
||||||
|
'**/*.generated.cjs',
|
||||||
|
...GENERATED_CJS_IGNORES,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── get-shit-done/bin/**/*.cjs + scripts/**/*.cjs ───────────────────────────
|
||||||
|
// CommonJS Node files: js.recommended + eslint-plugin-n + local plugin rules
|
||||||
|
// Type-aware via parserOptions.project=tsconfig.lint.json where applicable
|
||||||
|
{
|
||||||
|
files: ['get-shit-done/bin/**/*.cjs', 'scripts/**/*.cjs'],
|
||||||
|
plugins: {
|
||||||
|
n: pluginN,
|
||||||
|
local: localPlugin,
|
||||||
|
},
|
||||||
|
languageOptions: {
|
||||||
|
sourceType: 'commonjs',
|
||||||
|
globals: {
|
||||||
|
...globals.node,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...js.configs.recommended.rules,
|
||||||
|
// Generic quality rules
|
||||||
|
'no-var': 'error',
|
||||||
|
'prefer-const': 'warn',
|
||||||
|
'no-unused-vars': ['warn', {
|
||||||
|
argsIgnorePattern: '^_',
|
||||||
|
varsIgnorePattern: '^_',
|
||||||
|
caughtErrors: 'none',
|
||||||
|
}],
|
||||||
|
'no-empty': ['warn', { allowEmptyCatch: true }],
|
||||||
|
// Downgraded from recommended error → warn (pre-existing violations; follow-up to fix)
|
||||||
|
'no-useless-escape': 'warn',
|
||||||
|
'no-unsafe-finally': 'warn',
|
||||||
|
// eslint-plugin-n rules
|
||||||
|
'n/no-process-exit': 'warn',
|
||||||
|
// Local rules — warn for now; flip to error after cleanup phases
|
||||||
|
'local/no-source-grep': 'warn',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── tests/**/*.test.cjs ─────────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
files: ['tests/**/*.test.cjs'],
|
||||||
|
plugins: {
|
||||||
|
'no-only-tests': noOnlyTests,
|
||||||
|
local: localPlugin,
|
||||||
|
},
|
||||||
|
languageOptions: {
|
||||||
|
sourceType: 'commonjs',
|
||||||
|
globals: {
|
||||||
|
...globals.node,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...js.configs.recommended.rules,
|
||||||
|
'no-only-tests/no-only-tests': 'error',
|
||||||
|
// Timing anti-patterns — warn for now; flip to error after cleanup
|
||||||
|
'local/no-magic-sleep-in-tests': 'warn',
|
||||||
|
'local/no-elapsed-assertion': 'warn',
|
||||||
|
// Ban raw setTimeout sync + elapsed/duration-style assertions via no-restricted-syntax
|
||||||
|
'no-restricted-syntax': [
|
||||||
|
'warn',
|
||||||
|
{
|
||||||
|
selector: 'AwaitExpression > NewExpression[callee.name="Promise"] ArrowFunctionExpression CallExpression[callee.name="setTimeout"]',
|
||||||
|
message: 'Raw setTimeout used for synchronization in tests. Use proper async patterns instead.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: 'CallExpression[callee.object.name="Atomics"][callee.property.name="wait"]',
|
||||||
|
message: 'Atomics.wait() used as a sleep in tests. Use a proper async wait pattern instead.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'no-unused-vars': ['warn', {
|
||||||
|
argsIgnorePattern: '^_',
|
||||||
|
varsIgnorePattern: '^_',
|
||||||
|
caughtErrors: 'none',
|
||||||
|
}],
|
||||||
|
'no-empty': ['warn', { allowEmptyCatch: true }],
|
||||||
|
// Downgraded from recommended error → warn (pre-existing violations; follow-up to fix)
|
||||||
|
'no-useless-escape': 'warn',
|
||||||
|
'no-regex-spaces': 'warn',
|
||||||
|
'no-control-regex': 'warn',
|
||||||
|
'no-irregular-whitespace': 'warn',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -121,7 +121,7 @@ function scanQuickTasks(planDir) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let status = 'missing';
|
let status = 'missing';
|
||||||
let description = '';
|
const description = '';
|
||||||
|
|
||||||
if (summaryPath && fs.existsSync(summaryPath)) {
|
if (summaryPath && fs.existsSync(summaryPath)) {
|
||||||
let safeSum;
|
let safeSum;
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ function extractFrontmatter(content) {
|
|||||||
|
|
||||||
// Stack to track nested objects: [{obj, key, indent}]
|
// Stack to track nested objects: [{obj, key, indent}]
|
||||||
// obj = object to write to, key = current key collecting array items, indent = indentation level
|
// obj = object to write to, key = current key collecting array items, indent = indentation level
|
||||||
let stack = [{ obj: frontmatter, key: null, indent: -1 }];
|
const stack = [{ obj: frontmatter, key: null, indent: -1 }];
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
// Skip empty lines
|
// Skip empty lines
|
||||||
|
|||||||
@@ -985,7 +985,7 @@ function cmdInitMilestoneOp(cwd, raw) {
|
|||||||
// `phases clear` between milestones, on-disk dirs will be a subset of the
|
// `phases clear` between milestones, on-disk dirs will be a subset of the
|
||||||
// roadmap until each phase is materialized; reading from disk causes
|
// roadmap until each phase is materialized; reading from disk causes
|
||||||
// `all_phases_complete: true` to fire prematurely.
|
// `all_phases_complete: true` to fire prematurely.
|
||||||
let roadmapPhaseNumbers = [];
|
const roadmapPhaseNumbers = [];
|
||||||
try {
|
try {
|
||||||
const roadmapPath = path.join(planningDir(cwd), 'ROADMAP.md');
|
const roadmapPath = path.join(planningDir(cwd), 'ROADMAP.md');
|
||||||
const roadmapRaw = fs.readFileSync(roadmapPath, 'utf-8');
|
const roadmapRaw = fs.readFileSync(roadmapPath, 'utf-8');
|
||||||
|
|||||||
@@ -256,7 +256,7 @@ function applyBudget({ sections, budget, options = {} }) {
|
|||||||
const budgetUnderPressure = baseTokens > effectiveBudget;
|
const budgetUnderPressure = baseTokens > effectiveBudget;
|
||||||
|
|
||||||
// Available for content (reserve note slot when under pressure)
|
// Available for content (reserve note slot when under pressure)
|
||||||
let contentBudget = budgetUnderPressure
|
const contentBudget = budgetUnderPressure
|
||||||
? effectiveBudget - NOTE_RESERVE_TOKENS
|
? effectiveBudget - NOTE_RESERVE_TOKENS
|
||||||
: effectiveBudget;
|
: effectiveBudget;
|
||||||
|
|
||||||
|
|||||||
@@ -529,7 +529,7 @@ function cmdRoadmapAnnotateDependencies(cwd, phaseNum, raw) {
|
|||||||
// Patch ROADMAP.md
|
// Patch ROADMAP.md
|
||||||
let updated = false;
|
let updated = false;
|
||||||
withPlanningLock(cwd, () => {
|
withPlanningLock(cwd, () => {
|
||||||
let content = fs.readFileSync(roadmapPath, 'utf-8');
|
const content = fs.readFileSync(roadmapPath, 'utf-8');
|
||||||
|
|
||||||
// Find the phase section.
|
// Find the phase section.
|
||||||
// #3537: padding-tolerant fragment so the caller's resolved padded id
|
// #3537: padding-tolerant fragment so the caller's resolved padded id
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ function loadInstallExports() {
|
|||||||
const savedTestMode = process.env.GSD_TEST_MODE;
|
const savedTestMode = process.env.GSD_TEST_MODE;
|
||||||
if (savedTestMode === undefined) process.env.GSD_TEST_MODE = '1';
|
if (savedTestMode === undefined) process.env.GSD_TEST_MODE = '1';
|
||||||
try {
|
try {
|
||||||
// eslint-disable-next-line global-require -- lazy import to avoid running installer at module load
|
|
||||||
return require('../../../bin/install.js');
|
return require('../../../bin/install.js');
|
||||||
} finally {
|
} finally {
|
||||||
if (savedTestMode === undefined) delete process.env.GSD_TEST_MODE;
|
if (savedTestMode === undefined) delete process.env.GSD_TEST_MODE;
|
||||||
|
|||||||
@@ -945,7 +945,7 @@ function stripFrontmatter(content) {
|
|||||||
// Handles CRLF line endings and multiple stacked blocks (corruption recovery).
|
// Handles CRLF line endings and multiple stacked blocks (corruption recovery).
|
||||||
// Greedy: keeps stripping ---...--- blocks separated by optional whitespace.
|
// Greedy: keeps stripping ---...--- blocks separated by optional whitespace.
|
||||||
let result = content;
|
let result = content;
|
||||||
// eslint-disable-next-line no-constant-condition
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const stripped = result.replace(/^\s*---\r?\n[\s\S]*?\r?\n---\s*/, '');
|
const stripped = result.replace(/^\s*---\r?\n[\s\S]*?\r?\n---\s*/, '');
|
||||||
if (stripped === result) break;
|
if (stripped === result) break;
|
||||||
@@ -1001,7 +1001,7 @@ function acquireStateLock(statePath) {
|
|||||||
const startedAt = Date.now();
|
const startedAt = Date.now();
|
||||||
const sleepBuffer = new Int32Array(new SharedArrayBuffer(4)); // hoisted; value stays 0, pure Atomics.wait timeout target
|
const sleepBuffer = new Int32Array(new SharedArrayBuffer(4)); // hoisted; value stays 0, pure Atomics.wait timeout target
|
||||||
|
|
||||||
// eslint-disable-next-line no-constant-condition
|
|
||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
const fd = fs.openSync(lockPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY);
|
const fd = fs.openSync(lockPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY);
|
||||||
@@ -1391,7 +1391,7 @@ function cmdStatePlannedPhase(cwd, phaseNumber, planCount, raw) {
|
|||||||
|
|
||||||
// Update Total Plans in Phase
|
// Update Total Plans in Phase
|
||||||
if (planCount !== null && planCount !== undefined) {
|
if (planCount !== null && planCount !== undefined) {
|
||||||
let result = stateReplaceField(content, 'Total Plans in Phase', String(planCount));
|
const result = stateReplaceField(content, 'Total Plans in Phase', String(planCount));
|
||||||
if (result) { content = result; updated.push('Total Plans in Phase'); }
|
if (result) { content = result; updated.push('Total Plans in Phase'); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1403,7 +1403,7 @@ function cmdStatePlannedPhase(cwd, phaseNumber, planCount, raw) {
|
|||||||
|
|
||||||
// Update Last Activity Description
|
// Update Last Activity Description
|
||||||
{
|
{
|
||||||
let result = stateReplaceField(content, 'Last Activity Description', `Phase ${phaseNumber} planning complete — ${planCount || '?'} plans ready`);
|
const result = stateReplaceField(content, 'Last Activity Description', `Phase ${phaseNumber} planning complete — ${planCount || '?'} plans ready`);
|
||||||
if (result) { content = result; updated.push('Last Activity Description'); }
|
if (result) { content = result; updated.push('Last Activity Description'); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1429
package-lock.json
generated
1429
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
@@ -48,8 +48,15 @@
|
|||||||
"ws": "8.20.1"
|
"ws": "8.20.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.39.4",
|
||||||
"c8": "^11.0.0",
|
"c8": "^11.0.0",
|
||||||
"js-yaml": "^4.1.1"
|
"eslint": "^9.39.4",
|
||||||
|
"eslint-plugin-n": "^17.24.0",
|
||||||
|
"eslint-plugin-no-only-tests": "^3.4.0",
|
||||||
|
"globals": "^16.5.0",
|
||||||
|
"js-yaml": "^4.1.1",
|
||||||
|
"typescript": "^6.0.3",
|
||||||
|
"typescript-eslint": "^8.60.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"fallow": "^2.70.0"
|
"fallow": "^2.70.0"
|
||||||
@@ -64,9 +71,10 @@
|
|||||||
"prepublishOnly": "npm run build:hooks",
|
"prepublishOnly": "npm run build:hooks",
|
||||||
"pretest": "npm run lint:skill-deps",
|
"pretest": "npm run lint:skill-deps",
|
||||||
"pretest:coverage": "npm run lint:skill-deps",
|
"pretest:coverage": "npm run lint:skill-deps",
|
||||||
|
"lint": "eslint . --cache --cache-location node_modules/.cache/eslint/",
|
||||||
|
"lint:fix": "eslint . --fix",
|
||||||
"lint:descriptions": "node scripts/lint-descriptions.cjs",
|
"lint:descriptions": "node scripts/lint-descriptions.cjs",
|
||||||
"lint:skill-deps": "node scripts/lint-skill-deps.cjs",
|
"lint:skill-deps": "node scripts/lint-skill-deps.cjs",
|
||||||
"lint:tests": "node scripts/lint-no-source-grep.cjs",
|
|
||||||
"lint:test-file-count": "node scripts/lint-test-file-count.cjs",
|
"lint:test-file-count": "node scripts/lint-test-file-count.cjs",
|
||||||
"lint:pr-checks": "node scripts/lint-pr-check-project-dir.cjs",
|
"lint:pr-checks": "node scripts/lint-pr-check-project-dir.cjs",
|
||||||
"lint:changeset": "node scripts/changeset/lint.cjs",
|
"lint:changeset": "node scripts/changeset/lint.cjs",
|
||||||
|
|||||||
319
tests/eslint-rules.test.cjs
Normal file
319
tests/eslint-rules.test.cjs
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* eslint-rules.test.cjs
|
||||||
|
*
|
||||||
|
* RuleTester unit tests for the three local ESLint rules:
|
||||||
|
* - local/no-source-grep
|
||||||
|
* - local/no-magic-sleep-in-tests
|
||||||
|
* - local/no-elapsed-assertion
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { test, describe } = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const { RuleTester } = require('eslint');
|
||||||
|
|
||||||
|
const noSourceGrep = require('../eslint-rules/no-source-grep.cjs');
|
||||||
|
const noMagicSleepInTests = require('../eslint-rules/no-magic-sleep-in-tests.cjs');
|
||||||
|
const noElapsedAssertion = require('../eslint-rules/no-elapsed-assertion.cjs');
|
||||||
|
|
||||||
|
const ruleTester = new RuleTester({
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2022,
|
||||||
|
sourceType: 'commonjs',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── no-source-grep ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('no-source-grep rule', () => {
|
||||||
|
test('valid: readFileSync on .md file is allowed', () => {
|
||||||
|
ruleTester.run('no-source-grep', noSourceGrep, {
|
||||||
|
valid: [
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const content = fs.readFileSync(path.join(__dirname, '..', 'docs', 'readme.md'), 'utf-8');
|
||||||
|
content.includes('hello');
|
||||||
|
`,
|
||||||
|
filename: 'tests/foo.test.cjs',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const content = fs.readFileSync(path.join(__dirname, '..', 'get-shit-done', 'workflows', 'config.json'), 'utf-8');
|
||||||
|
content.includes('key');
|
||||||
|
`,
|
||||||
|
filename: 'tests/foo.test.cjs',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
invalid: [],
|
||||||
|
});
|
||||||
|
assert.ok(true, 'no-source-grep valid cases passed');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('invalid: readFileSync on .cjs source file followed by .includes()', () => {
|
||||||
|
ruleTester.run('no-source-grep', noSourceGrep, {
|
||||||
|
valid: [],
|
||||||
|
invalid: [
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const src = fs.readFileSync(path.join(__dirname, '..', 'get-shit-done', 'bin', 'lib', 'core.cjs'), 'utf-8');
|
||||||
|
src.includes('someFunction');
|
||||||
|
`,
|
||||||
|
filename: 'tests/foo.test.cjs',
|
||||||
|
errors: [{ messageId: 'noSourceGrep' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
assert.ok(true, 'no-source-grep invalid case detected');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('invalid: readFileSync on .cjs source file followed by .match()', () => {
|
||||||
|
ruleTester.run('no-source-grep', noSourceGrep, {
|
||||||
|
valid: [],
|
||||||
|
invalid: [
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const src = fs.readFileSync(path.join(__dirname, '..', 'lib', 'foo.cjs'), 'utf-8');
|
||||||
|
src.match(/pattern/);
|
||||||
|
`,
|
||||||
|
filename: 'tests/foo.test.cjs',
|
||||||
|
errors: [{ messageId: 'noSourceGrep' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
assert.ok(true, 'no-source-grep match case detected');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('valid: file with allow-test-rule annotation is exempt', () => {
|
||||||
|
ruleTester.run('no-source-grep', noSourceGrep, {
|
||||||
|
valid: [
|
||||||
|
{
|
||||||
|
// The allow annotation exempts the whole file
|
||||||
|
code: `
|
||||||
|
// allow-test-rule: pending migration
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const src = fs.readFileSync(path.join(__dirname, '..', 'get-shit-done', 'bin', 'lib', 'core.cjs'), 'utf-8');
|
||||||
|
src.includes('someFunction');
|
||||||
|
`,
|
||||||
|
filename: 'tests/foo.test.cjs',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
invalid: [],
|
||||||
|
});
|
||||||
|
assert.ok(true, 'no-source-grep allow-test-rule annotation works');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('valid: require() of a .cjs file is allowed (not readFileSync)', () => {
|
||||||
|
ruleTester.run('no-source-grep', noSourceGrep, {
|
||||||
|
valid: [
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
const mod = require('../get-shit-done/bin/lib/core.cjs');
|
||||||
|
mod.someMethod();
|
||||||
|
`,
|
||||||
|
filename: 'tests/foo.test.cjs',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
invalid: [],
|
||||||
|
});
|
||||||
|
assert.ok(true, 'no-source-grep require() is allowed');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── no-magic-sleep-in-tests ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('no-magic-sleep-in-tests rule', () => {
|
||||||
|
test('valid: setTimeout used outside tests (no-op since rule only applies to *.test.cjs)', () => {
|
||||||
|
// Rule only applies to *.test.cjs files; a non-test filename is always valid
|
||||||
|
ruleTester.run('no-magic-sleep-in-tests', noMagicSleepInTests, {
|
||||||
|
valid: [
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
const delay = new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
`,
|
||||||
|
filename: 'scripts/some-script.cjs',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
invalid: [],
|
||||||
|
});
|
||||||
|
assert.ok(true, 'no-magic-sleep-in-tests does not apply outside test files');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('invalid: Atomics.wait() in test file', () => {
|
||||||
|
ruleTester.run('no-magic-sleep-in-tests', noMagicSleepInTests, {
|
||||||
|
valid: [],
|
||||||
|
invalid: [
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
const shared = new SharedArrayBuffer(4);
|
||||||
|
const arr = new Int32Array(shared);
|
||||||
|
Atomics.wait(arr, 0, 0, 100);
|
||||||
|
`,
|
||||||
|
filename: 'tests/some.test.cjs',
|
||||||
|
errors: [{ messageId: 'atomicsWaitSleep' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
assert.ok(true, 'no-magic-sleep-in-tests flags Atomics.wait()');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('invalid: setTimeout used for synchronization in Promise in test file', () => {
|
||||||
|
ruleTester.run('no-magic-sleep-in-tests', noMagicSleepInTests, {
|
||||||
|
valid: [],
|
||||||
|
invalid: [
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
async function waitABit() {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 50));
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
filename: 'tests/some.test.cjs',
|
||||||
|
errors: [{ messageId: 'setTimeoutSync' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
assert.ok(true, 'no-magic-sleep-in-tests flags setTimeout in Promise');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('valid: setTimeout with callback (not synchronization pattern) in test file', () => {
|
||||||
|
// A setTimeout with no second arg or with a callback that does real work
|
||||||
|
// is allowed. The rule only flags the await-new-Promise(setTimeout) pattern.
|
||||||
|
ruleTester.run('no-magic-sleep-in-tests', noMagicSleepInTests, {
|
||||||
|
valid: [
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
function doSomethingLater(cb) {
|
||||||
|
setTimeout(cb, 100);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
filename: 'tests/some.test.cjs',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
invalid: [],
|
||||||
|
});
|
||||||
|
assert.ok(true, 'no-magic-sleep-in-tests allows simple callback setTimeout');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── no-elapsed-assertion ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('no-elapsed-assertion rule', () => {
|
||||||
|
test('valid: assert on non-timing property', () => {
|
||||||
|
ruleTester.run('no-elapsed-assertion', noElapsedAssertion, {
|
||||||
|
valid: [
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const result = { count: 5 };
|
||||||
|
assert.equal(result.count, 5);
|
||||||
|
`,
|
||||||
|
filename: 'tests/foo.test.cjs',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
assert.ok(result.success);
|
||||||
|
`,
|
||||||
|
filename: 'tests/foo.test.cjs',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
invalid: [],
|
||||||
|
});
|
||||||
|
assert.ok(true, 'no-elapsed-assertion valid cases passed');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('invalid: assert on .elapsed property', () => {
|
||||||
|
ruleTester.run('no-elapsed-assertion', noElapsedAssertion, {
|
||||||
|
valid: [],
|
||||||
|
invalid: [
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const result = { elapsed: 150 };
|
||||||
|
assert.ok(result.elapsed < 200);
|
||||||
|
`,
|
||||||
|
filename: 'tests/foo.test.cjs',
|
||||||
|
errors: [{ messageId: 'noElapsedAssertion' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
assert.ok(true, 'no-elapsed-assertion flags assert on .elapsed');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('invalid: assert on .duration property', () => {
|
||||||
|
ruleTester.run('no-elapsed-assertion', noElapsedAssertion, {
|
||||||
|
valid: [],
|
||||||
|
invalid: [
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
assert.equal(stats.duration, 100);
|
||||||
|
`,
|
||||||
|
filename: 'tests/foo.test.cjs',
|
||||||
|
errors: [{ messageId: 'noElapsedAssertion' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
assert.ok(true, 'no-elapsed-assertion flags assert on .duration');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('invalid: assert on .took property', () => {
|
||||||
|
ruleTester.run('no-elapsed-assertion', noElapsedAssertion, {
|
||||||
|
valid: [],
|
||||||
|
invalid: [
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
assert.ok(result.took < 500);
|
||||||
|
`,
|
||||||
|
filename: 'tests/foo.test.cjs',
|
||||||
|
errors: [{ messageId: 'noElapsedAssertion' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
assert.ok(true, 'no-elapsed-assertion flags assert on .took');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('invalid: assert on .ms property', () => {
|
||||||
|
ruleTester.run('no-elapsed-assertion', noElapsedAssertion, {
|
||||||
|
valid: [],
|
||||||
|
invalid: [
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
assert.ok(result.ms > 0);
|
||||||
|
`,
|
||||||
|
filename: 'tests/foo.test.cjs',
|
||||||
|
errors: [{ messageId: 'noElapsedAssertion' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
assert.ok(true, 'no-elapsed-assertion flags assert on .ms');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('invalid: assert.equal with timing comparison', () => {
|
||||||
|
ruleTester.run('no-elapsed-assertion', noElapsedAssertion, {
|
||||||
|
valid: [],
|
||||||
|
invalid: [
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
assert.equal(result.elapsed > 0, true);
|
||||||
|
`,
|
||||||
|
filename: 'tests/foo.test.cjs',
|
||||||
|
errors: [{ messageId: 'noElapsedAssertion' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
assert.ok(true, 'no-elapsed-assertion flags assert.equal with timing comparison');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -145,8 +145,8 @@ describe('Gemini Markdown Processor', () => {
|
|||||||
|
|
||||||
const result = convertClaudeToGeminiAgent(input);
|
const result = convertClaudeToGeminiAgent(input);
|
||||||
|
|
||||||
assert.match(result, /^ - read_file$/m, 'Read should still map to Gemini read_file');
|
assert.match(result, /^ {2}- read_file$/m, 'Read should still map to Gemini read_file');
|
||||||
assert.doesNotMatch(result, /^ - ask_user$/m, 'ask_user must not be emitted as a Gemini tool');
|
assert.doesNotMatch(result, /^ {2}- ask_user$/m, 'ask_user must not be emitted as a Gemini tool');
|
||||||
assert.doesNotMatch(result, /\bAskUserQuestion\b/, 'Claude tool references must be neutralized');
|
assert.doesNotMatch(result, /\bAskUserQuestion\b/, 'Claude tool references must be neutralized');
|
||||||
assert.doesNotMatch(result, /\bask_user\b/, 'lowercase ask_user references must be neutralized');
|
assert.doesNotMatch(result, /\bask_user\b/, 'lowercase ask_user references must be neutralized');
|
||||||
assert.match(result, /conversational prompting/, 'body should use runtime-neutral wording');
|
assert.match(result, /conversational prompting/, 'body should use runtime-neutral wording');
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ const SKILL_RUNTIMES = [
|
|||||||
// ─── Helper functions ─────────────────────────────────────────────────────────
|
// ─── Helper functions ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function stripAnsi(str) {
|
function stripAnsi(str) {
|
||||||
// eslint-disable-next-line no-control-regex
|
|
||||||
return str.replace(/\x1b\[[0-9;]*m/g, '');
|
return str.replace(/\x1b\[[0-9;]*m/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ function parseWorkflowStructure(src) {
|
|||||||
// A new top-level key ends the `on:` block
|
// A new top-level key ends the `on:` block
|
||||||
if (/^[a-zA-Z]/.test(line) && !line.startsWith(' ')) { inOn = false; continue; }
|
if (/^[a-zA-Z]/.test(line) && !line.startsWith(' ')) { inOn = false; continue; }
|
||||||
// Direct children of `on:` are trigger names at 2-space indent
|
// Direct children of `on:` are trigger names at 2-space indent
|
||||||
const m = line.match(/^ ([a-zA-Z_][a-zA-Z0-9_]*):/);
|
const m = line.match(/^ {2}([a-zA-Z_][a-zA-Z0-9_]*):/);
|
||||||
if (m) onTriggers.push(m[1]);
|
if (m) onTriggers.push(m[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -45,17 +45,17 @@ function parseWorkflowStructure(src) {
|
|||||||
let inPrPaths = false;
|
let inPrPaths = false;
|
||||||
let inPullRequest = false;
|
let inPullRequest = false;
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
if (/^ pull_request:/.test(line)) { inPullRequest = true; continue; }
|
if (/^ {2}pull_request:/.test(line)) { inPullRequest = true; continue; }
|
||||||
if (inPullRequest) {
|
if (inPullRequest) {
|
||||||
if (/^ paths:/.test(line)) { inPrPaths = true; continue; }
|
if (/^ {4}paths:/.test(line)) { inPrPaths = true; continue; }
|
||||||
if (inPrPaths) {
|
if (inPrPaths) {
|
||||||
const m = line.match(/^ - '(.+)'/);
|
const m = line.match(/^ {6}- '(.+)'/);
|
||||||
if (m) { prPaths.push(m[1]); continue; }
|
if (m) { prPaths.push(m[1]); continue; }
|
||||||
// End of paths list
|
// End of paths list
|
||||||
if (/^ [a-zA-Z]/.test(line)) inPrPaths = false;
|
if (/^ {4}[a-zA-Z]/.test(line)) inPrPaths = false;
|
||||||
}
|
}
|
||||||
// End of pull_request block
|
// End of pull_request block
|
||||||
if (/^ [a-zA-Z]/.test(line) && !line.startsWith(' ')) inPullRequest = false;
|
if (/^ {2}[a-zA-Z]/.test(line) && !line.startsWith(' ')) inPullRequest = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,46 +67,46 @@ function parseWorkflowStructure(src) {
|
|||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
// Step boundary: 6-space "- name:" or "- uses:"
|
// Step boundary: 6-space "- name:" or "- uses:"
|
||||||
if (/^ - name:/.test(line)) {
|
if (/^ {6}- name:/.test(line)) {
|
||||||
if (currentStep && runLines.length) {
|
if (currentStep && runLines.length) {
|
||||||
currentStep.run = runLines.join('\n');
|
currentStep.run = runLines.join('\n');
|
||||||
}
|
}
|
||||||
if (currentStep) steps.push(currentStep);
|
if (currentStep) steps.push(currentStep);
|
||||||
currentStep = { name: line.replace(/^ - name:\s*/, '').trim() };
|
currentStep = { name: line.replace(/^ {6}- name:\s*/, '').trim() };
|
||||||
inRun = false;
|
inRun = false;
|
||||||
runLines = [];
|
runLines = [];
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (/^ - uses:/.test(line)) {
|
if (/^ {6}- uses:/.test(line)) {
|
||||||
if (currentStep && runLines.length) {
|
if (currentStep && runLines.length) {
|
||||||
currentStep.run = runLines.join('\n');
|
currentStep.run = runLines.join('\n');
|
||||||
}
|
}
|
||||||
if (currentStep) steps.push(currentStep);
|
if (currentStep) steps.push(currentStep);
|
||||||
currentStep = { uses: line.replace(/^ - uses:\s*/, '').trim() };
|
currentStep = { uses: line.replace(/^ {6}- uses:\s*/, '').trim() };
|
||||||
inRun = false;
|
inRun = false;
|
||||||
runLines = [];
|
runLines = [];
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// uses: field inside a named step (e.g. "- name: Foo\n uses: actions/...")
|
// uses: field inside a named step (e.g. "- name: Foo\n uses: actions/...")
|
||||||
if (currentStep && /^ uses:\s/.test(line)) {
|
if (currentStep && /^ {8}uses:\s/.test(line)) {
|
||||||
currentStep.uses = line.replace(/^ uses:\s*/, '').trim();
|
currentStep.uses = line.replace(/^ {8}uses:\s*/, '').trim();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// run: block inside a step
|
// run: block inside a step
|
||||||
if (currentStep && /^ run:\s*\|/.test(line)) {
|
if (currentStep && /^ {8}run:\s*\|/.test(line)) {
|
||||||
inRun = true;
|
inRun = true;
|
||||||
runLines = [];
|
runLines = [];
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (currentStep && /^ run:\s*(?!\|)/.test(line)) {
|
if (currentStep && /^ {8}run:\s*(?!\|)/.test(line)) {
|
||||||
// Inline run (no |)
|
// Inline run (no |)
|
||||||
currentStep.run = line.replace(/^ run:\s*/, '').trim();
|
currentStep.run = line.replace(/^ {8}run:\s*/, '').trim();
|
||||||
inRun = false;
|
inRun = false;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (inRun) {
|
if (inRun) {
|
||||||
// Lines deeper than 8 spaces belong to the run block
|
// Lines deeper than 8 spaces belong to the run block
|
||||||
if (/^ /.test(line) || line.trim() === '') {
|
if (/^ {10}/.test(line) || line.trim() === '') {
|
||||||
runLines.push(line);
|
runLines.push(line);
|
||||||
} else {
|
} else {
|
||||||
inRun = false;
|
inRun = false;
|
||||||
|
|||||||
29
tsconfig.lint.json
Normal file
29
tsconfig.lint.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "commonjs",
|
||||||
|
"strict": false
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"get-shit-done/bin/lib/**/*.cjs"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"get-shit-done/bin/lib/command-aliases.cjs",
|
||||||
|
"get-shit-done/bin/lib/configuration.cjs",
|
||||||
|
"get-shit-done/bin/lib/decisions.cjs",
|
||||||
|
"get-shit-done/bin/lib/phase-lifecycle.cjs",
|
||||||
|
"get-shit-done/bin/lib/plan-scan.cjs",
|
||||||
|
"get-shit-done/bin/lib/project-root.cjs",
|
||||||
|
"get-shit-done/bin/lib/schema-detect.cjs",
|
||||||
|
"get-shit-done/bin/lib/secrets.cjs",
|
||||||
|
"get-shit-done/bin/lib/state-document.cjs",
|
||||||
|
"get-shit-done/bin/lib/validate.cjs",
|
||||||
|
"get-shit-done/bin/lib/workstream-inventory-builder.cjs",
|
||||||
|
"get-shit-done/bin/lib/workstream-name-policy.cjs",
|
||||||
|
"tests/**/*",
|
||||||
|
"node_modules/**/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user