Clarify lint guardrail configuration

This commit is contained in:
murashit 2026-06-07 14:00:27 +09:00
parent e6daf9db11
commit 19be43215e
3 changed files with 84 additions and 101 deletions

View file

@ -43,26 +43,6 @@ const unsafeIteratorRestrictions = [
message: "Avoid reading iterator.next().value directly; use for...of or inspect the typed IteratorResult first.",
},
];
const chatStateRestrictions = [
{
selector: "AssignmentExpression[left.type='MemberExpression'][left.object.name='state']",
message: "Route ChatState updates through ChatStateStore.dispatch().",
},
{
selector: "AssignmentExpression[left.type='MemberExpression'][left.object.type='MemberExpression'][left.object.property.name='state']",
message: "Route ChatState updates through ChatStateStore.dispatch().",
},
{
selector:
"CallExpression[callee.property.name=/^(push|set|delete|clear|add)$/][callee.object.type='MemberExpression'][callee.object.object.name='state']",
message: "Clone ChatState collections and update them through ChatStateStore.dispatch().",
},
{
selector:
"CallExpression[callee.property.name=/^(push|set|delete|clear|add)$/][callee.object.type='MemberExpression'][callee.object.object.type='MemberExpression'][callee.object.object.property.name='state']",
message: "Clone ChatState collections and update them through ChatStateStore.dispatch().",
},
];
const pureChatModelRestrictions = [
{
selector: "CallExpression[callee.object.name='Date'][callee.property.name='now']",
@ -90,11 +70,9 @@ const pureChatModelRestrictions = [
},
];
const chatImperativeDomBridgeFiles = [
"src/features/chat/ui/message-stream/**/*.{ts,tsx}",
"src/features/chat/ui/composer.tsx",
"src/features/chat/ui/goal-banner.tsx",
"src/features/chat/ui/message-stream/markdown-renderer.ts",
"src/features/chat/ui/message-stream/rendered-markdown-links.ts",
"src/features/chat/ui/message-stream/**/*.tsx",
"src/features/chat/ui/scroll.ts",
"src/features/chat/ui/shell.tsx",
"src/features/chat/ui/tool-result.tsx",
@ -136,9 +114,64 @@ const codexPanelEslintPlugin = {
};
},
},
"no-chat-state-direct-mutation": {
meta: {
type: "problem",
docs: {
description: "Disallow direct ChatState mutation in chat modules.",
},
messages: {
assign: "Route ChatState updates through ChatStateStore.dispatch().",
mutateCollection: "Clone ChatState collections and update them through ChatStateStore.dispatch().",
},
schema: [],
},
create(context) {
const mutatingCollectionMethods = new Set(["add", "clear", "delete", "push", "set"]);
return {
AssignmentExpression(node) {
if (isChatStateMember(node.left)) context.report({ node: node.left, messageId: "assign" });
},
CallExpression(node) {
if (!isMemberExpression(node.callee)) return;
const method = staticPropertyName(node.callee.property);
if (!method || !mutatingCollectionMethods.has(method)) return;
if (isChatStateMember(node.callee.object)) context.report({ node: node.callee, messageId: "mutateCollection" });
},
};
},
},
},
};
function isChatStateMember(node) {
if (!isMemberExpression(node)) return false;
let current = node;
while (isMemberExpression(current)) {
if (isIdentifier(current.object, "state")) return true;
if (isThisStateMember(current.object)) return true;
current = current.object;
}
return false;
}
function isThisStateMember(node) {
return isMemberExpression(node) && node.object?.type === "ThisExpression" && staticPropertyName(node.property) === "state";
}
function isMemberExpression(node) {
return node?.type === "MemberExpression";
}
function isIdentifier(node, name) {
return node?.type === "Identifier" && node.name === name;
}
function staticPropertyName(node) {
return node?.type === "Identifier" ? node.name : node?.type === "Literal" && typeof node.value === "string" ? node.value : null;
}
function findInitializerCallbackReference(root, name) {
let reference = null;
@ -300,8 +333,8 @@ export default defineConfig([
...unsafeIteratorRestrictions,
...imperativeDomRestrictions,
...preactFormRestrictions,
...chatStateRestrictions,
],
"codex-panel/no-chat-state-direct-mutation": "error",
},
},
{
@ -312,8 +345,8 @@ export default defineConfig([
...removedChatStateEscapeHatchRestrictions,
...unsafeIteratorRestrictions,
...preactFormRestrictions,
...chatStateRestrictions,
],
"codex-panel/no-chat-state-direct-mutation": "error",
},
},
{
@ -336,9 +369,9 @@ export default defineConfig([
...unsafeIteratorRestrictions,
...imperativeDomRestrictions,
...preactFormRestrictions,
...chatStateRestrictions,
...pureChatModelRestrictions,
],
"codex-panel/no-chat-state-direct-mutation": "error",
},
},
{

View file

@ -2,6 +2,16 @@ import { spawnSync } from "node:child_process";
const args = new Set(process.argv.slice(2));
const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
const checks = [
{ local: "typecheck", ci: "typecheck:ci", localPhase: "parallel" },
{ local: "test", ci: "test:ci", localPhase: "parallel" },
{ local: "lint:ts", ci: "lint:ts:ci", localPhase: "parallel" },
{ local: "lint:css", ci: "lint:css", localPhase: "parallel" },
{ local: "format:check", ci: "format:check:ci", localPhase: "parallel" },
{ local: "build:styles:check", ci: "build:styles:check", localPhase: "parallel" },
{ local: "unused", ci: "unused", localPhase: "parallel" },
{ local: "build", ci: "build", localPhase: "sequential" },
];
for (const arg of args) {
if (arg !== "--ci") {
@ -11,12 +21,11 @@ for (const arg of args) {
}
if (args.has("--ci")) {
for (const script of ["typecheck:ci", "test:ci", "lint:ts:ci", "lint:css", "format:check:ci", "build:styles:check", "unused", "build"]) {
run(npmCommand, ["run", script]);
}
for (const check of checks) run(npmCommand, ["run", check.ci]);
} else {
run("node", ["scripts/run-parallel.mjs", "typecheck", "test", "lint:ts", "lint:css", "format:check", "build:styles:check", "unused"]);
run(npmCommand, ["run", "build"]);
const parallelScripts = checks.filter((check) => check.localPhase === "parallel").map((check) => check.local);
run("node", ["scripts/run-parallel.mjs", ...parallelScripts]);
for (const check of checks.filter((check) => check.localPhase === "sequential")) run(npmCommand, ["run", check.local]);
}
function run(command, args) {

View file

@ -4,33 +4,13 @@ export default {
extends: ["stylelint-config-standard"],
plugins: [noSpecificityWhere],
rules: {
"codex-panel/no-specificity-where": [
true,
{
severity: "warning",
},
],
"color-named": [
"never",
{
severity: "warning",
},
],
"color-no-hex": [
true,
{
severity: "warning",
},
],
"codex-panel/no-specificity-where": true,
"color-named": "never",
"color-no-hex": true,
"custom-property-empty-line-before": null,
"declaration-block-no-redundant-longhand-properties": null,
"declaration-empty-line-before": null,
"declaration-no-important": [
true,
{
severity: "warning",
},
],
"declaration-no-important": true,
"declaration-property-value-disallowed-list": [
{
"/^(?:background(?:-color)?|border(?:-(?:block|inline|top|right|bottom|left))?(?:-color)?|box-shadow|caret-color|color|fill|outline(?:-color)?|stroke|text-shadow)$/":
@ -45,7 +25,6 @@ export default {
},
{
message: "Use Obsidian or Codex Panel design tokens instead of hardcoded colors or typography.",
severity: "warning",
},
],
"declaration-property-unit-disallowed-list": [
@ -55,67 +34,29 @@ export default {
},
{
message: "Prefer Obsidian or Codex Panel spacing/size tokens for layout dimensions.",
severity: "warning",
},
],
"function-disallowed-list": [
["rgb", "rgba", "hsl", "hsla", "hwb", "lab", "lch", "oklab", "oklch"],
{
severity: "warning",
},
],
"keyframes-name-pattern": [
"^codex-panel-[a-z0-9-]+$",
{
severity: "warning",
},
],
"max-nesting-depth": [
2,
{
severity: "warning",
},
],
"function-disallowed-list": ["rgb", "rgba", "hsl", "hsla", "hwb", "lab", "lch", "oklab", "oklch"],
"keyframes-name-pattern": "^codex-panel-[a-z0-9-]+$",
"max-nesting-depth": 2,
"no-descending-specificity": null,
"selector-class-pattern": null,
"selector-max-compound-selectors": [
4,
{
severity: "warning",
},
],
"selector-max-specificity": [
"0,4,0",
{
severity: "warning",
},
],
"selector-max-compound-selectors": 4,
"selector-max-specificity": "0,4,0",
"selector-max-type": [
0,
{
ignoreTypes: ["dd", "dt", "pre", "span", "summary", "svg", "ul"],
severity: "warning",
},
],
"selector-max-id": [
0,
{
severity: "warning",
},
],
"selector-max-id": 0,
"selector-disallowed-list": [
["/:has\\(/"],
{
message: "Avoid :has() because it can cause broad selector invalidation.",
severity: "warning",
},
],
"selector-max-universal": [
0,
{
severity: "warning",
},
],
"selector-max-universal": 0,
"selector-not-notation": null,
},
};