@deslop/coding-standards 0.1.761 → 0.1.785
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -0
- package/dist/install.js +1 -1
- package/dist/oxlint.d.ts +7 -1
- package/dist/oxlint.js +118 -126
- package/package.json +1 -1
- package/skills/design/SKILL.md +73 -61
- package/skills/engineering/SKILL.md +31 -17
- package/skills/testing/SKILL.md +6 -4
package/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Coding standards
|
|
2
|
+
|
|
3
|
+
Strict Oxlint preset and repository skill installer. Requires Node 26+; skill installation runs inside a Git repository, including linked worktrees.
|
|
4
|
+
|
|
5
|
+
## Install skills
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
vpx @deslop/coding-standards@latest
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
| Result | Scope |
|
|
12
|
+
| -------------------------------------- | -------------------------------------------------------------------------------------------------- |
|
|
13
|
+
| Engineering, design and testing skills | Git root's existing `.agents`, `.claude` and `.codex` directories; creates `.agents` if none exist |
|
|
14
|
+
| Refresh | Replaces only those three skill names; preserves other skills and global configuration |
|
|
15
|
+
| Boundary | Rejects destinations escaping the repository or overlapping global configuration |
|
|
16
|
+
|
|
17
|
+
No repository dependency, manifest/lock change or lint configuration change. Treat installed skill copies as read-only; refresh through the CLI.
|
|
18
|
+
|
|
19
|
+
## Use the lint preset
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
vp add -D @deslop/coding-standards @effect/tsgo
|
|
23
|
+
vp exec effect-tsgo patch --no-typescript --oxlint
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import {defineConfig} from 'vite-plus'
|
|
28
|
+
import {oxlint} from '@deslop/coding-standards'
|
|
29
|
+
|
|
30
|
+
export default defineConfig({lint: {extends: [oxlint]}})
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The preset uses Effect-tsgo's patched Oxlint and compatible tsgolint; keep the patch command in the project's prepare script. Match upstream supported tool versions. The skill installer neither configures lint nor upgrades/patches its toolchain.
|
|
34
|
+
|
|
35
|
+
## Working here
|
|
36
|
+
|
|
37
|
+
| Owner | Decision |
|
|
38
|
+
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
39
|
+
| `src/` | Flat package structure; rules in `src/rules`, no separate plugin or install-test harness |
|
|
40
|
+
| `skills/` | Maintained engineering/design/testing sources; refresh repository copies from the TypeScript CLI, no build needed |
|
|
41
|
+
| Lint rules | Prefer an equivalent maintained rule before custom enforcement; consumer code conforms to strict standards, not the reverse. Fix broken/imprecise rules, not inconvenient valid diagnostics |
|
|
42
|
+
| Rule proof | Existing public CLI regression cases, with accepted/rejected controls; no severity/exclusion workaround |
|
|
43
|
+
|
|
44
|
+
## Rejected shortcuts
|
|
45
|
+
|
|
46
|
+
| Candidate | Reason |
|
|
47
|
+
| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
|
|
48
|
+
| Blanket `unknown` type ban | Opaque SDK/framework contracts legitimately need it; narrow/decode before use instead of fabricating a shape |
|
|
49
|
+
| Functional preset or dependency just to replace `no-let` | The preset conflicts; a selective plugin adds dependencies for a tiny rule |
|
|
50
|
+
| Syntax-only generator-failure rule | Factory-call syntax does not establish yieldability; the existing type-aware Effect rule owns that check |
|
|
51
|
+
|
|
52
|
+
With workspace dependencies installed, from the repository root:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
vp test run tools/coding-standards/src/rules/rules.test.ts
|
|
56
|
+
node tools/coding-standards/src/install.ts
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Skill inspiration
|
|
60
|
+
|
|
61
|
+
| Source | Used / boundary |
|
|
62
|
+
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
63
|
+
| [Matt Pocock's engineering skills](https://github.com/mattpocock/skills/tree/d81f3a183412e71a5b1e84ca21bc1a35eea03a60/skills/engineering) | Caller-first contracts, causal debugging and one uncertain behavior at a time; no universal TDD |
|
|
64
|
+
| [Cursor / pstack](https://github.com/cursor/plugins/tree/e43c7ee26e0038c6c1fa8380dd34ce86ff94cb2a/pstack) | Interface rationale, focused bug replay and defensible benchmarks; no playbook-stage machinery |
|
|
65
|
+
| [Kit Langton's Effect skill](https://github.com/kitlangton/skills/tree/71195e36024ebd2e66c5beac946e81eafd5abc43/skills/effect) | Schema construction/optionality, checked against actual [Effect 4 source](https://github.com/Effect-TS/effect/tree/67ba4e46a11ccda0b6761578bfd22c04ae00167d/packages/effect/src) |
|
|
66
|
+
| [Vercel's agent skills](https://github.com/vercel-labs/agent-skills/tree/063bee94c3f4df8453406c830b0a7df0f2860278/skills) | Independent I/O and narrow subscriptions, translated to Effect/atoms; no manual React memoization |
|
|
67
|
+
| [Anthropic frontend-design](https://github.com/anthropics/skills/tree/main/skills/frontend-design), [Taste Skill](https://github.com/Leonxlnx/taste-skill), [Vercel design guidelines](https://vercel.com/design/guidelines) | Task-led composition, typography and useful copy; no forced novelty, arbitrary aesthetic bans or plugin bundle |
|
|
68
|
+
| [Emil Kowalski's skills](https://github.com/emilkowalski/skills/tree/e8a175de22ae1e49370fc144c1f3bb9aeedf988d/skills) | Realistic layout stress, interruptible motion and mobile checks; no fixed variant picker or audit fan-out. Performance caveats checked against [Motion docs](https://motion.dev/docs/performance) |
|
|
69
|
+
| [Dillon Mulroy's skills](https://github.com/dmmulroy/skills/tree/8603380821fee6a77c82639f364ce8fe4f5a92be) and [anti-slop](https://github.com/dmmulroy/anti-slop/tree/c44ef22ca116d0ba62a3ff663a0bd13a3f3fa40b) | Reviewed Effect/error and cleanup approaches; retain our own service layout and ownership |
|
|
70
|
+
| [Awesome DESIGN.md](https://github.com/VoltAgent/awesome-design-md) and [Impeccable](https://github.com/pbakaus/impeccable) | Reviewed reference collections; community brand analyses are inspiration, not authoritative/current tokens |
|
|
71
|
+
|
|
72
|
+
These are selectively adapted ideas, not imported frameworks. Concrete behavior, existing stack and settled user decisions govern the maintained skills.
|
package/dist/install.js
CHANGED
|
@@ -24685,7 +24685,7 @@ const runWith = (command, config) => {
|
|
|
24685
24685
|
};
|
|
24686
24686
|
//#endregion
|
|
24687
24687
|
//#region package.json
|
|
24688
|
-
var version = "0.1.
|
|
24688
|
+
var version = "0.1.785";
|
|
24689
24689
|
//#endregion
|
|
24690
24690
|
//#region src/install.ts
|
|
24691
24691
|
const cli = pipe(make("deslop-coding-standards", {}, fnUntraced(function* () {
|
package/dist/oxlint.d.ts
CHANGED
|
@@ -4093,7 +4093,6 @@ export declare const oxlint: {
|
|
|
4093
4093
|
'@deslop/coding-standards/no-double-nullish-check': "error";
|
|
4094
4094
|
'@deslop/coding-standards/no-effect-property-arrow': "error";
|
|
4095
4095
|
'@deslop/coding-standards/no-error-message-assertion': "error";
|
|
4096
|
-
'@deslop/coding-standards/no-fail-in-generator': "error";
|
|
4097
4096
|
'@deslop/coding-standards/no-fake-ref-state': "error";
|
|
4098
4097
|
'@deslop/coding-standards/no-hand-written-guard': "error";
|
|
4099
4098
|
'@deslop/coding-standards/no-let': "error";
|
|
@@ -4320,6 +4319,7 @@ export declare const oxlint: {
|
|
|
4320
4319
|
'max-depth': ["error", number];
|
|
4321
4320
|
'max-nested-callbacks': ["error", number];
|
|
4322
4321
|
'no-cond-assign': "error";
|
|
4322
|
+
'no-constant-binary-expression': "error";
|
|
4323
4323
|
'no-continue': "error";
|
|
4324
4324
|
'no-control-regex': "error";
|
|
4325
4325
|
'no-debugger': "error";
|
|
@@ -4335,6 +4335,7 @@ export declare const oxlint: {
|
|
|
4335
4335
|
'no-invalid-regexp': "error";
|
|
4336
4336
|
'no-iterator': "error";
|
|
4337
4337
|
'no-lonely-if': "error";
|
|
4338
|
+
'no-loss-of-precision': "error";
|
|
4338
4339
|
'no-misleading-character-class': "error";
|
|
4339
4340
|
'no-multi-assign': "error";
|
|
4340
4341
|
'no-negated-condition': "error";
|
|
@@ -4504,6 +4505,9 @@ export declare const oxlint: {
|
|
|
4504
4505
|
'no-unneeded-ternary': "error";
|
|
4505
4506
|
'no-unreachable-loop': "error";
|
|
4506
4507
|
'no-unsafe-finally': "error";
|
|
4508
|
+
'no-unsafe-optional-chaining': ["error", {
|
|
4509
|
+
disallowArithmeticOperators: true;
|
|
4510
|
+
}];
|
|
4507
4511
|
'no-unused-expressions': "error";
|
|
4508
4512
|
'no-useless-assignment': "error";
|
|
4509
4513
|
'no-useless-backreference': "error";
|
|
@@ -4515,6 +4519,7 @@ export declare const oxlint: {
|
|
|
4515
4519
|
'no-useless-escape': "error";
|
|
4516
4520
|
'no-useless-rename': "error";
|
|
4517
4521
|
'no-useless-return': "error";
|
|
4522
|
+
'no-var': "error";
|
|
4518
4523
|
'no-void': "error";
|
|
4519
4524
|
'object-shorthand': "error";
|
|
4520
4525
|
'oxc/branches-sharing-code': "error";
|
|
@@ -4528,6 +4533,7 @@ export declare const oxlint: {
|
|
|
4528
4533
|
'prefer-template': "error";
|
|
4529
4534
|
'require-unicode-regexp': "error";
|
|
4530
4535
|
'react-doctor/no-call-component-as-function': "error";
|
|
4536
|
+
'react-doctor/no-controlled-input-value-without-state-update': "error";
|
|
4531
4537
|
'react-doctor/no-create-context-in-render': "error";
|
|
4532
4538
|
'react-doctor/no-default-props': "error";
|
|
4533
4539
|
'react-doctor/no-inline-exhaustive-style': "error";
|
package/dist/oxlint.js
CHANGED
|
@@ -612,7 +612,7 @@ Object.freeze({
|
|
|
612
612
|
}
|
|
613
613
|
});
|
|
614
614
|
//#endregion
|
|
615
|
-
//#region ../../node_modules/.pnpm/oxlint@1.85.0_oxlint-tsgolint@7.0.2003_vite-plus@1.0.0_@opentelemetry+api@1.9.1_@types+
|
|
615
|
+
//#region ../../node_modules/.pnpm/oxlint@1.85.0_oxlint-tsgolint@7.0.2003_vite-plus@1.0.0_@opentelemetry+api@1.9.1_@types+_bd1fc584f29238a6938d999624a6e509/node_modules/oxlint/dist/index.js
|
|
616
616
|
/**
|
|
617
617
|
* Define an Oxlint configuration with type inference.
|
|
618
618
|
*
|
|
@@ -668,14 +668,6 @@ function importedMember(input) {
|
|
|
668
668
|
source: "effect"
|
|
669
669
|
}) && (input.propertyName === void 0 || contains(memberName(input.node), input.propertyName));
|
|
670
670
|
}
|
|
671
|
-
function effectBuilderName(input) {
|
|
672
|
-
const builder = input.node.type === "CallExpression" ? input.node.callee : input.node;
|
|
673
|
-
return builder.type === "MemberExpression" && importedMember({
|
|
674
|
-
context: input.context,
|
|
675
|
-
importedName: "Effect",
|
|
676
|
-
node: builder
|
|
677
|
-
}) ? memberName(builder) : none();
|
|
678
|
-
}
|
|
679
671
|
function isSchemaOperationName(name) {
|
|
680
672
|
return /^(?:(?:decode|encode)(?:Unknown)?(?:Effect|Exit|Option|Promise|Result|Sync)|asserts|is)$/u.test(name);
|
|
681
673
|
}
|
|
@@ -892,33 +884,10 @@ const noArrayWrapTernary = defineRule({
|
|
|
892
884
|
});
|
|
893
885
|
//#endregion
|
|
894
886
|
//#region src/rules/no-constant-function.ts
|
|
895
|
-
function returnsConstant(node) {
|
|
896
|
-
const returned = returnedExpression(node);
|
|
897
|
-
return isNotNullish(returned) && (returned.type === "Literal" || returned.type === "Identifier" || returned.type === "MemberExpression" || returned.type === "TemplateLiteral" && isArrayEmpty(returned.expressions));
|
|
898
|
-
}
|
|
899
|
-
function functionScope(input) {
|
|
900
|
-
if (input.scope.block === input.node) return some(input.scope);
|
|
901
|
-
return findFirst(input.scope.childScopes, (child) => child.block === input.node);
|
|
902
|
-
}
|
|
903
|
-
function scopeReferences(scope) {
|
|
904
|
-
return appendAll(scope.references, flatMap$1(scope.childScopes, scopeReferences));
|
|
905
|
-
}
|
|
906
|
-
function isImportedVariable(variable) {
|
|
907
|
-
return isArrayNonEmpty(variable.defs) && every(variable.defs, (definition) => definition.type === "ImportBinding");
|
|
908
|
-
}
|
|
909
|
-
function resolvesToImport(reference) {
|
|
910
|
-
return pipe(variableFromScope({
|
|
911
|
-
name: reference.identifier.name,
|
|
912
|
-
scope: reference.from
|
|
913
|
-
}), exists(isImportedVariable));
|
|
914
|
-
}
|
|
915
887
|
function isConstantFunction(input) {
|
|
916
|
-
if (input.node.async || input.node.generator || isNotNullish(input.node.typeParameters)) return false;
|
|
917
|
-
|
|
918
|
-
return
|
|
919
|
-
node: input.node,
|
|
920
|
-
scope: input.context.sourceCode.getScope(input.node)
|
|
921
|
-
}), exists((scope) => every(scopeReferences(scope), resolvesToImport))) && pipe(bindingName(input.node), flatMap((name) => variableFromScope({
|
|
888
|
+
if (input.node.async || input.node.generator || isArrayNonEmpty(input.node.params) || isNotNullish(input.node.typeParameters)) return false;
|
|
889
|
+
const returned = returnedExpression(input.node);
|
|
890
|
+
return isNotNullish(returned) && (returned.type === "Literal" && !hasProperty(returned, "regex") || returned.type === "TemplateLiteral" && isArrayEmpty(returned.expressions)) && pipe(bindingName(input.node), flatMap((name) => variableFromScope({
|
|
922
891
|
name,
|
|
923
892
|
scope: input.context.sourceCode.getScope(input.node)
|
|
924
893
|
})), exists((variable) => every(filter$1(variable.references, (reference) => reference.isRead()), (reference) => reference.identifier.parent.type === "CallExpression" && reference.identifier.parent.callee === reference.identifier)));
|
|
@@ -928,20 +897,15 @@ function isConstBound(node) {
|
|
|
928
897
|
}
|
|
929
898
|
const noConstantFunction = defineRule({
|
|
930
899
|
create: (context) => {
|
|
931
|
-
const spans = make$2(empty());
|
|
932
|
-
function mark(node) {
|
|
933
|
-
update(spans, append(node));
|
|
934
|
-
}
|
|
935
900
|
function report(node) {
|
|
936
901
|
if (isConstantFunction({
|
|
937
902
|
context,
|
|
938
|
-
node
|
|
939
|
-
spans: get$3(spans)
|
|
903
|
+
node
|
|
940
904
|
}) && !singleUseThunk({
|
|
941
905
|
context,
|
|
942
906
|
node
|
|
943
907
|
})) context.report({
|
|
944
|
-
message: "
|
|
908
|
+
message: "Inline this argument-free literal return at its call sites.",
|
|
945
909
|
node
|
|
946
910
|
});
|
|
947
911
|
}
|
|
@@ -952,31 +916,33 @@ const noConstantFunction = defineRule({
|
|
|
952
916
|
"FunctionDeclaration:exit": report,
|
|
953
917
|
"FunctionExpression:exit": (node) => {
|
|
954
918
|
if (isConstBound(node)) report(node);
|
|
955
|
-
}
|
|
956
|
-
JSXElement: mark,
|
|
957
|
-
JSXFragment: mark,
|
|
958
|
-
ThisExpression: mark
|
|
919
|
+
}
|
|
959
920
|
};
|
|
960
921
|
},
|
|
961
922
|
meta: { type: "problem" }
|
|
962
923
|
});
|
|
963
924
|
//#endregion
|
|
964
925
|
//#region src/rules/no-deep-pipe.ts
|
|
965
|
-
function isPipeCall(node) {
|
|
966
|
-
return node.type === "CallExpression" && node.callee.type === "Identifier" &&
|
|
926
|
+
function isPipeCall(context, node) {
|
|
927
|
+
return node.type === "CallExpression" && node.callee.type === "Identifier" && isImportBinding({
|
|
928
|
+
context,
|
|
929
|
+
importedName: "pipe",
|
|
930
|
+
node: node.callee,
|
|
931
|
+
source: "effect"
|
|
932
|
+
});
|
|
967
933
|
}
|
|
968
|
-
function enclosingPipes(node) {
|
|
934
|
+
function enclosingPipes(context, node) {
|
|
969
935
|
if (node.type === "Program" || contains$1([
|
|
970
936
|
"ArrowFunctionExpression",
|
|
971
937
|
"FunctionDeclaration",
|
|
972
938
|
"FunctionExpression"
|
|
973
939
|
], node.type)) return 0;
|
|
974
|
-
const nested = isPipeCall(node.parent) && some$1(node.parent.arguments, (argument) => argument === node);
|
|
975
|
-
return enclosingPipes(node.parent) + (nested ? 1 : 0);
|
|
940
|
+
const nested = isPipeCall(context, node.parent) && some$1(node.parent.arguments, (argument) => argument === node);
|
|
941
|
+
return enclosingPipes(context, node.parent) + (nested ? 1 : 0);
|
|
976
942
|
}
|
|
977
943
|
const noDeepPipe = defineRule({
|
|
978
944
|
create: (context) => ({ CallExpression: (node) => {
|
|
979
|
-
if (isPipeCall(node) && enclosingPipes(node) >= 3) context.report({
|
|
945
|
+
if (isPipeCall(context, node) && enclosingPipes(context, node) >= 3) context.report({
|
|
980
946
|
message: "Flatten this pipeline; pipe calls nest at most three deep.",
|
|
981
947
|
node
|
|
982
948
|
});
|
|
@@ -985,23 +951,61 @@ const noDeepPipe = defineRule({
|
|
|
985
951
|
});
|
|
986
952
|
//#endregion
|
|
987
953
|
//#region src/rules/no-destructured-parameter.ts
|
|
988
|
-
function
|
|
989
|
-
|
|
990
|
-
if (
|
|
954
|
+
function namedTuple(input) {
|
|
955
|
+
if (contains$1(input.seen, input.node)) return false;
|
|
956
|
+
if (input.node.type === "TSTupleType") return isArrayNonEmpty(input.node.elementTypes) && every(input.node.elementTypes, (element) => element.type === "TSNamedTupleMember");
|
|
957
|
+
if (input.node.type === "TSParenthesizedType" || input.node.type === "TSTypeOperator" && input.node.operator === "readonly") return namedTuple({
|
|
958
|
+
...input,
|
|
959
|
+
node: input.node.typeAnnotation,
|
|
960
|
+
seen: append(input.seen, input.node)
|
|
961
|
+
});
|
|
962
|
+
if (input.node.type !== "TSTypeReference" || input.node.typeName.type !== "Identifier") return false;
|
|
963
|
+
return pipe(variableFromScope({
|
|
964
|
+
name: input.node.typeName.name,
|
|
965
|
+
scope: input.context.sourceCode.getScope(input.node)
|
|
966
|
+
}), exists((variable) => some$1(variable.defs, (definition) => definition.node.type === "TSTypeAliasDeclaration" && namedTuple({
|
|
967
|
+
context: input.context,
|
|
968
|
+
node: definition.node.typeAnnotation,
|
|
969
|
+
seen: append(input.seen, input.node)
|
|
970
|
+
}))));
|
|
971
|
+
}
|
|
972
|
+
function component(node) {
|
|
973
|
+
const declared = node.type === "ArrowFunctionExpression" ? void 0 : node.id;
|
|
974
|
+
const assigned = node.parent.type === "VariableDeclarator" && node.parent.id.type === "Identifier" ? node.parent.id : void 0;
|
|
975
|
+
const name = declared ?? assigned;
|
|
976
|
+
if (name === void 0 || !/^[A-Z]/u.test(name.name) || node.params.length !== 1 || node.body === null) return false;
|
|
977
|
+
if (node.body.type === "JSXElement" || node.body.type === "JSXFragment") return true;
|
|
978
|
+
return node.body.type === "BlockStatement" && some$1(node.body.body, (statement) => statement.type === "ReturnStatement" && (statement.argument?.type === "JSXElement" || statement.argument?.type === "JSXFragment"));
|
|
979
|
+
}
|
|
980
|
+
function isPattern(input) {
|
|
981
|
+
const target = input.node.type === "AssignmentPattern" ? input.node.left : input.node;
|
|
982
|
+
if (target.type === "ArrayPattern") return !exists(fromNullishOr(target.typeAnnotation), (annotation) => namedTuple({
|
|
983
|
+
context: input.context,
|
|
984
|
+
node: annotation.typeAnnotation,
|
|
985
|
+
seen: []
|
|
986
|
+
}));
|
|
991
987
|
if (target.type !== "ObjectPattern") return false;
|
|
992
988
|
const named = filter$1(target.properties, (property) => property.type === "Property");
|
|
993
|
-
return !(named.length === 1 && named[0]?.key.type === "Identifier" && named[0].key.name === "ref" && some$1(target.properties, (property) => property.type === "RestElement"));
|
|
989
|
+
return !(input.component && named.length === 1 && named[0]?.key.type === "Identifier" && named[0].key.name === "ref" && some$1(target.properties, (property) => property.type === "RestElement"));
|
|
994
990
|
}
|
|
995
991
|
const noDestructuredParameter = defineRule({
|
|
996
992
|
create: (context) => {
|
|
997
993
|
function reportParameters(node) {
|
|
998
|
-
for (const parameter of filter$1(node.params,
|
|
994
|
+
for (const parameter of filter$1(node.params, (candidate) => isPattern({
|
|
995
|
+
component: component(node),
|
|
996
|
+
context,
|
|
997
|
+
node: candidate
|
|
998
|
+
}))) context.report({
|
|
999
999
|
message: "Take the value whole and read its fields instead of destructuring it.",
|
|
1000
1000
|
node: parameter
|
|
1001
1001
|
});
|
|
1002
1002
|
}
|
|
1003
1003
|
function reportLoopVariable(node) {
|
|
1004
|
-
if (node.left.type === "VariableDeclaration" && some$1(node.left.declarations, (declarator) => isPattern(
|
|
1004
|
+
if (node.left.type === "VariableDeclaration" && some$1(node.left.declarations, (declarator) => isPattern({
|
|
1005
|
+
component: false,
|
|
1006
|
+
context,
|
|
1007
|
+
node: declarator.id
|
|
1008
|
+
}))) context.report({
|
|
1005
1009
|
message: "Take the value whole and read its fields instead of destructuring it.",
|
|
1006
1010
|
node: node.left
|
|
1007
1011
|
});
|
|
@@ -1125,50 +1129,26 @@ const noErrorMessageAssertion = defineRule({
|
|
|
1125
1129
|
meta: { type: "problem" }
|
|
1126
1130
|
});
|
|
1127
1131
|
//#endregion
|
|
1128
|
-
//#region src/rules/no-fail-in-generator.ts
|
|
1129
|
-
function insideEffectGenerator(input) {
|
|
1130
|
-
if (input.node.type === "Program") return false;
|
|
1131
|
-
if ((input.node.type === "ArrowFunctionExpression" || input.node.type === "FunctionDeclaration" || input.node.type === "FunctionExpression") && !input.node.generator) return false;
|
|
1132
|
-
if (input.node.type === "FunctionExpression" && input.node.generator && input.node.parent.type === "CallExpression" && exists(effectBuilderName({
|
|
1133
|
-
context: input.context,
|
|
1134
|
-
node: input.node.parent.callee
|
|
1135
|
-
}), (name) => contains$1([
|
|
1136
|
-
"fn",
|
|
1137
|
-
"fnUntraced",
|
|
1138
|
-
"gen"
|
|
1139
|
-
], name))) return true;
|
|
1140
|
-
return insideEffectGenerator({
|
|
1141
|
-
context: input.context,
|
|
1142
|
-
node: input.node.parent
|
|
1143
|
-
});
|
|
1144
|
-
}
|
|
1145
|
-
const noFailInGenerator = defineRule({
|
|
1146
|
-
create: (context) => ({ CallExpression: (node) => {
|
|
1147
|
-
if (node.callee.type === "MemberExpression" && importedMember({
|
|
1148
|
-
context,
|
|
1149
|
-
importedName: "Effect",
|
|
1150
|
-
node: node.callee,
|
|
1151
|
-
propertyName: "fail"
|
|
1152
|
-
}) && node.parent.type === "YieldExpression" && node.parent.delegate && node.arguments[0]?.type !== "NewExpression" && insideEffectGenerator({
|
|
1153
|
-
context,
|
|
1154
|
-
node
|
|
1155
|
-
})) context.report({
|
|
1156
|
-
message: "Yield the error instead: return yield* E.make(...).",
|
|
1157
|
-
node
|
|
1158
|
-
});
|
|
1159
|
-
} }),
|
|
1160
|
-
meta: { type: "problem" }
|
|
1161
|
-
});
|
|
1162
|
-
//#endregion
|
|
1163
1132
|
//#region src/rules/no-fake-ref-state.ts
|
|
1164
1133
|
function propertyName(node) {
|
|
1165
1134
|
if (!node.computed && node.key.type === "Identifier") return some(node.key.name);
|
|
1166
1135
|
if (node.key.type === "Literal" && isString(node.key.value)) return some(node.key.value);
|
|
1167
1136
|
return none();
|
|
1168
1137
|
}
|
|
1138
|
+
function hasUnusedSetter(input) {
|
|
1139
|
+
const declaration = input.node.parent;
|
|
1140
|
+
if (declaration.type !== "VariableDeclarator" || declaration.id.type !== "ArrayPattern") return false;
|
|
1141
|
+
const setter = declaration.id.elements[1];
|
|
1142
|
+
if (isNullish(setter)) return true;
|
|
1143
|
+
if (setter.type !== "Identifier") return false;
|
|
1144
|
+
return pipe(variableFromScope({
|
|
1145
|
+
name: setter.name,
|
|
1146
|
+
scope: input.context.sourceCode.getScope(setter)
|
|
1147
|
+
}), exists((variable) => !some$1(variable.references, (reference) => reference.isRead())));
|
|
1148
|
+
}
|
|
1169
1149
|
function isFakeRefState(input) {
|
|
1170
|
-
if (!isReactUseState(input) || input.node.arguments[0]?.type !== "ArrowFunctionExpression" || input.node.arguments[0].body.type !== "ObjectExpression") return false;
|
|
1171
|
-
return some$1(input.node.arguments[0].body.properties, (property) => property.type === "Property" && contains(propertyName(property), "current"));
|
|
1150
|
+
if (!isReactUseState(input) || input.node.arguments[0]?.type !== "ArrowFunctionExpression" || input.node.arguments[0].body.type !== "ObjectExpression" || input.node.arguments[0].body.properties.length !== 1 || !hasUnusedSetter(input)) return false;
|
|
1151
|
+
return some$1(input.node.arguments[0].body.properties, (property) => property.type === "Property" && contains(propertyName(property), "current") && property.value.type === "Literal" && property.value.value === null);
|
|
1172
1152
|
}
|
|
1173
1153
|
const noFakeRefState = defineRule({
|
|
1174
1154
|
create: (context) => ({ CallExpression: (node) => {
|
|
@@ -1492,18 +1472,43 @@ function insideCycleType(input) {
|
|
|
1492
1472
|
node: input.node.parent
|
|
1493
1473
|
});
|
|
1494
1474
|
}
|
|
1495
|
-
function
|
|
1496
|
-
if (
|
|
1497
|
-
|
|
1498
|
-
|
|
1475
|
+
function typeDeclarations(input) {
|
|
1476
|
+
if (input.name.type === "Identifier") return pipe(variableFromScope({
|
|
1477
|
+
name: input.name.name,
|
|
1478
|
+
scope: input.context.sourceCode.getScope(input.name)
|
|
1479
|
+
}), flatMap((variable) => variable.scope.block.type === "ClassDeclaration" && some$1(variable.defs, (definition) => definition.node === variable.scope.block) ? variableFromScope({
|
|
1480
|
+
name: variable.name,
|
|
1481
|
+
scope: variable.scope.upper
|
|
1482
|
+
}) : some(variable)), map((variable) => map$1(variable.defs, (definition) => definition.node)), getOrElse(() => empty()));
|
|
1483
|
+
if (input.name.type !== "TSQualifiedName") return [];
|
|
1484
|
+
const name = input.name;
|
|
1485
|
+
return pipe(typeDeclarations({
|
|
1486
|
+
context: input.context,
|
|
1487
|
+
name: name.left
|
|
1488
|
+
}), flatMap$1((declaration) => {
|
|
1489
|
+
if (declaration.type !== "TSModuleDeclaration" || declaration.body === null) return [];
|
|
1490
|
+
return pipe(declaration.body.body, map$1(statementDeclaration), filter$1((node) => node !== null));
|
|
1491
|
+
}), filter$1((declaration) => (declaration.type === "TSTypeAliasDeclaration" || declaration.type === "TSModuleDeclaration") && declaration.id.type === "Identifier" && declaration.id.name === name.right.name));
|
|
1499
1492
|
}
|
|
1500
1493
|
function shapeLiterals(input) {
|
|
1501
1494
|
if (input.type.type === "TSTypeLiteral") return [input.type];
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1495
|
+
if (input.type.type === "TSUnionType" || input.type.type === "TSIntersectionType") return flatMap$1(input.type.types, (type) => shapeLiterals({
|
|
1496
|
+
...input,
|
|
1497
|
+
type
|
|
1498
|
+
}));
|
|
1499
|
+
if (input.type.type === "TSParenthesizedType") return shapeLiterals({
|
|
1500
|
+
...input,
|
|
1501
|
+
type: input.type.typeAnnotation
|
|
1502
|
+
});
|
|
1503
|
+
if (input.type.type !== "TSTypeReference") return [];
|
|
1504
|
+
return pipe(typeDeclarations({
|
|
1505
|
+
context: input.context,
|
|
1506
|
+
name: input.type.typeName
|
|
1507
|
+
}), filter$1((declaration) => declaration.type === "TSTypeAliasDeclaration"), filter$1((alias) => !contains$1(input.seen, alias)), flatMap$1((alias) => shapeLiterals({
|
|
1508
|
+
...input,
|
|
1509
|
+
seen: append(input.seen, alias),
|
|
1505
1510
|
type: alias.typeAnnotation
|
|
1506
|
-
})))
|
|
1511
|
+
})));
|
|
1507
1512
|
}
|
|
1508
1513
|
const noReadonlyTypeSyntax = defineRule({
|
|
1509
1514
|
create: (context) => {
|
|
@@ -1512,7 +1517,6 @@ const noReadonlyTypeSyntax = defineRule({
|
|
|
1512
1517
|
program: context.sourceCode.ast
|
|
1513
1518
|
});
|
|
1514
1519
|
const shapes = make$2(empty());
|
|
1515
|
-
const aliases = make$2(empty());
|
|
1516
1520
|
const properties = make$2(empty());
|
|
1517
1521
|
const methods = make$2(empty());
|
|
1518
1522
|
function exempt(node) {
|
|
@@ -1532,7 +1536,7 @@ const noReadonlyTypeSyntax = defineRule({
|
|
|
1532
1536
|
},
|
|
1533
1537
|
"Program:exit": () => {
|
|
1534
1538
|
const literals = pipe(get$3(shapes), flatMap$1((shape) => shapeLiterals({
|
|
1535
|
-
|
|
1539
|
+
context,
|
|
1536
1540
|
seen: [],
|
|
1537
1541
|
type: shape
|
|
1538
1542
|
})));
|
|
@@ -1572,9 +1576,6 @@ const noReadonlyTypeSyntax = defineRule({
|
|
|
1572
1576
|
TSPropertySignature: (node) => {
|
|
1573
1577
|
update(properties, append(node));
|
|
1574
1578
|
},
|
|
1575
|
-
TSTypeAliasDeclaration: (node) => {
|
|
1576
|
-
update(aliases, append(node));
|
|
1577
|
-
},
|
|
1578
1579
|
TSTypeOperator: (node) => {
|
|
1579
1580
|
if (node.operator === "readonly" && !exempt(node)) context.report({
|
|
1580
1581
|
message: "Use a mutable type shape.",
|
|
@@ -1597,7 +1598,7 @@ function isExported(node) {
|
|
|
1597
1598
|
if (node.parent.type === "ExportNamedDeclaration" || node.parent.type === "ExportDefaultDeclaration") return true;
|
|
1598
1599
|
return node.parent.type === "VariableDeclarator" && node.parent.parent.type === "VariableDeclaration" && node.parent.parent.parent.type === "ExportNamedDeclaration";
|
|
1599
1600
|
}
|
|
1600
|
-
function inferredIdentically
|
|
1601
|
+
function inferredIdentically(input) {
|
|
1601
1602
|
const text = input.context.sourceCode.getText;
|
|
1602
1603
|
const body = input.node.body;
|
|
1603
1604
|
if (body === null) return false;
|
|
@@ -1620,7 +1621,7 @@ const noRedundantReturnType = defineRule({
|
|
|
1620
1621
|
const frame = last(get$3(frames));
|
|
1621
1622
|
update(frames, dropRight(1));
|
|
1622
1623
|
if (node.async || node.generator || isNullish(node.returnType)) return;
|
|
1623
|
-
if (inferredIdentically
|
|
1624
|
+
if (inferredIdentically({
|
|
1624
1625
|
context,
|
|
1625
1626
|
node,
|
|
1626
1627
|
returns: match(frame, {
|
|
@@ -1710,14 +1711,6 @@ const noRedundantUseRefNullType = defineRule({
|
|
|
1710
1711
|
});
|
|
1711
1712
|
//#endregion
|
|
1712
1713
|
//#region src/rules/no-redundant-variable-annotation.ts
|
|
1713
|
-
function isStringLiteral(node) {
|
|
1714
|
-
return node.type === "Literal" && isString(node.value);
|
|
1715
|
-
}
|
|
1716
|
-
function inferredIdentically(input) {
|
|
1717
|
-
const text = input.context.sourceCode.getText;
|
|
1718
|
-
if (input.init.type === "TSAsExpression") return text(input.init.typeAnnotation) === text(input.type);
|
|
1719
|
-
return input.init.type === "ConditionalExpression" && input.type.type === "TSStringKeyword" && isStringLiteral(input.init.consequent) && isStringLiteral(input.init.alternate);
|
|
1720
|
-
}
|
|
1721
1714
|
const noRedundantVariableAnnotation = defineRule({
|
|
1722
1715
|
create: (context) => ({ VariableDeclarator: (node) => {
|
|
1723
1716
|
const annotation = node.id.typeAnnotation;
|
|
@@ -1729,11 +1722,7 @@ const noRedundantVariableAnnotation = defineRule({
|
|
|
1729
1722
|
});
|
|
1730
1723
|
return;
|
|
1731
1724
|
}
|
|
1732
|
-
if (
|
|
1733
|
-
context,
|
|
1734
|
-
init: node.init,
|
|
1735
|
-
type: annotation.typeAnnotation
|
|
1736
|
-
})) context.report({
|
|
1725
|
+
if (node.init.type === "TSAsExpression" && context.sourceCode.getText(node.init.typeAnnotation) === context.sourceCode.getText(annotation.typeAnnotation)) context.report({
|
|
1737
1726
|
message: "Drop this annotation; inference gives the same type.",
|
|
1738
1727
|
node: annotation
|
|
1739
1728
|
});
|
|
@@ -2153,7 +2142,7 @@ function schemaDefinitionMember(node) {
|
|
|
2153
2142
|
return none();
|
|
2154
2143
|
}
|
|
2155
2144
|
function isSchemaDefinitionName(name) {
|
|
2156
|
-
return !isSchemaOperationName(name) && !/^to[A-Z]/u.test(name);
|
|
2145
|
+
return !isSchemaOperationName(name) && !/^(?:is|to)[A-Z]/u.test(name) && !/^makeFilter(?:Group)?$/u.test(name);
|
|
2157
2146
|
}
|
|
2158
2147
|
function isSchemaDefinition(input) {
|
|
2159
2148
|
if (input.node.type === "CallExpression" && input.node.callee.type === "Identifier" && isImportBinding({
|
|
@@ -2361,7 +2350,6 @@ const oxlint = defineConfig({
|
|
|
2361
2350
|
"@deslop/coding-standards/no-double-nullish-check": "error",
|
|
2362
2351
|
"@deslop/coding-standards/no-effect-property-arrow": "error",
|
|
2363
2352
|
"@deslop/coding-standards/no-error-message-assertion": "error",
|
|
2364
|
-
"@deslop/coding-standards/no-fail-in-generator": "error",
|
|
2365
2353
|
"@deslop/coding-standards/no-fake-ref-state": "error",
|
|
2366
2354
|
"@deslop/coding-standards/no-hand-written-guard": "error",
|
|
2367
2355
|
"@deslop/coding-standards/no-let": "error",
|
|
@@ -2638,6 +2626,7 @@ const oxlint = defineConfig({
|
|
|
2638
2626
|
"max-depth": ["error", 3],
|
|
2639
2627
|
"max-nested-callbacks": ["error", 6],
|
|
2640
2628
|
"no-cond-assign": "error",
|
|
2629
|
+
"no-constant-binary-expression": "error",
|
|
2641
2630
|
"no-continue": "error",
|
|
2642
2631
|
"no-control-regex": "error",
|
|
2643
2632
|
"no-debugger": "error",
|
|
@@ -2651,6 +2640,7 @@ const oxlint = defineConfig({
|
|
|
2651
2640
|
"no-invalid-regexp": "error",
|
|
2652
2641
|
"no-iterator": "error",
|
|
2653
2642
|
"no-lonely-if": "error",
|
|
2643
|
+
"no-loss-of-precision": "error",
|
|
2654
2644
|
"no-misleading-character-class": "error",
|
|
2655
2645
|
"no-multi-assign": "error",
|
|
2656
2646
|
"no-negated-condition": "error",
|
|
@@ -2888,6 +2878,7 @@ const oxlint = defineConfig({
|
|
|
2888
2878
|
"no-unneeded-ternary": "error",
|
|
2889
2879
|
"no-unreachable-loop": "error",
|
|
2890
2880
|
"no-unsafe-finally": "error",
|
|
2881
|
+
"no-unsafe-optional-chaining": ["error", { disallowArithmeticOperators: true }],
|
|
2891
2882
|
"no-unused-expressions": "error",
|
|
2892
2883
|
"no-useless-assignment": "error",
|
|
2893
2884
|
"no-useless-backreference": "error",
|
|
@@ -2899,6 +2890,7 @@ const oxlint = defineConfig({
|
|
|
2899
2890
|
"no-useless-escape": "error",
|
|
2900
2891
|
"no-useless-rename": "error",
|
|
2901
2892
|
"no-useless-return": "error",
|
|
2893
|
+
"no-var": "error",
|
|
2902
2894
|
"no-void": "error",
|
|
2903
2895
|
"object-shorthand": "error",
|
|
2904
2896
|
"oxc/branches-sharing-code": "error",
|
|
@@ -2910,6 +2902,7 @@ const oxlint = defineConfig({
|
|
|
2910
2902
|
"prefer-template": "error",
|
|
2911
2903
|
"require-unicode-regexp": "error",
|
|
2912
2904
|
"react-doctor/no-call-component-as-function": "error",
|
|
2905
|
+
"react-doctor/no-controlled-input-value-without-state-update": "error",
|
|
2913
2906
|
"react-doctor/no-create-context-in-render": "error",
|
|
2914
2907
|
"react-doctor/no-default-props": "error",
|
|
2915
2908
|
"react-doctor/no-inline-exhaustive-style": "error",
|
|
@@ -3048,7 +3041,6 @@ var oxlint_default = definePlugin({
|
|
|
3048
3041
|
"no-double-nullish-check": noDoubleNullishCheck,
|
|
3049
3042
|
"no-effect-property-arrow": noEffectPropertyArrow,
|
|
3050
3043
|
"no-error-message-assertion": noErrorMessageAssertion,
|
|
3051
|
-
"no-fail-in-generator": noFailInGenerator,
|
|
3052
3044
|
"no-fake-ref-state": noFakeRefState,
|
|
3053
3045
|
"no-hand-written-guard": noHandWrittenGuard,
|
|
3054
3046
|
"no-let": noLet,
|
package/package.json
CHANGED
package/skills/design/SKILL.md
CHANGED
|
@@ -5,103 +5,115 @@ description: 'UI and UX rules for screens and visual explanations. Use when buil
|
|
|
5
5
|
|
|
6
6
|
# Design
|
|
7
7
|
|
|
8
|
-
**Scope.** Follow repository-specific CODING_STANDARDS.md
|
|
8
|
+
**Scope.** Follow repository-specific CODING_STANDARDS.md and engineering.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
## Decide
|
|
11
11
|
|
|
12
|
-
**
|
|
12
|
+
**Direction.** Start from the actual task, audience, content and settled preferences. Make the next user action and its relevant content dominant, not generic "premium" styling. When a reference is useful, inspect the relevant accepted one for transferable hierarchy, density and interaction choices; distinguish observed details from inferred ones. Community brand analyses are inspiration, not verified current tokens. Keep durable visual choices at their owner, not repeated briefs. Ask only about a consequential tradeoff the evidence cannot settle; another agent cannot establish user taste.
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
<Empty><EmptyTitle>No runs yet</EmptyTitle><Button onClick={() => startRun()}>Run</Button></Empty>
|
|
18
|
-
// bad — "without hardcoding those values the desing/ui/ux need to be consistent with the rest of the app"
|
|
19
|
-
<span className="rounded-full bg-[#eef] px-[7px] text-[11px]">Draft</span>
|
|
20
|
-
<div className="flex flex-col items-center gap-2 p-6 text-muted-foreground">No runs yet</div>
|
|
21
|
-
```
|
|
14
|
+
**One meaning.** Each visible element must add information, enable a distinct action or clarify a relationship. Delete repeated headings, summaries, counts, status badges, decorative wrappers and controls that say or do the same thing. Remove explanations that merely restate the visible example/state. Keyboard shortcuts, screen-reader labels and responsive placement are not redundant visible controls; preserve their function.
|
|
15
|
+
|
|
16
|
+
**Scan-first.** Put the task, relevant content and next action where they can be identified without reading a paragraph. Use stable visual roles, not another card/badge/icon for every fact. Expose needed detail on demand, never hide a consequential warning or essential action.
|
|
22
17
|
|
|
23
|
-
|
|
18
|
+
| Content | Preferred treatment |
|
|
19
|
+
| -------------------------- | ------------------------------------------------------------------------- |
|
|
20
|
+
| Comparable records/options | Aligned rows/columns; consistent order, units and metadata placement |
|
|
21
|
+
| Familiar action | Conventional icon-only control with an accessible name; one visible owner |
|
|
22
|
+
| Ambiguous action | Clear text label, not an invented/guessed icon or redundant icon + text |
|
|
23
|
+
| Explanation | Real state/example first; text only for missing context or consequences |
|
|
24
24
|
|
|
25
25
|
```tsx
|
|
26
|
-
//
|
|
27
|
-
<
|
|
28
|
-
//
|
|
29
|
-
<
|
|
26
|
+
// one visible action, named for assistive technology
|
|
27
|
+
<Button aria-label="Rename" size="icon"><PencilIcon aria-hidden /></Button>
|
|
28
|
+
// redundant label for an already-obvious action
|
|
29
|
+
<Button><PencilIcon />Rename</Button>
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
Do not remove necessary names, status, comfortable hit targets or instructions merely to make a screen smaller.
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
// good
|
|
36
|
-
<StarIcon aria-label="Default" />
|
|
37
|
-
// bad — "i don't want refresh buttons or stuffs like that in the ui/app"
|
|
38
|
-
<Button onClick={refetch}><RefreshIcon /></Button>
|
|
39
|
-
<Badge><StarIcon /> Default</Badge> // "Don't have icons/emoji and text that say the same thing"
|
|
40
|
-
```
|
|
34
|
+
## Build
|
|
41
35
|
|
|
42
|
-
**
|
|
36
|
+
**Canonical components.** Reuse the existing renderer, states and interactions for the same role. Inspect its relevant source/API, not a full catalogue; add missing primitives through the repository registry command. Fix wrong shared patterns at their owner when delivering retained code, not before an exploratory preview. Without a sibling, use the supplied reference and component defaults within settled preferences.
|
|
43
37
|
|
|
44
38
|
```tsx
|
|
45
|
-
//
|
|
46
|
-
<
|
|
47
|
-
//
|
|
48
|
-
<
|
|
39
|
+
// same model-selection control used elsewhere
|
|
40
|
+
<ModelPicker value={model} onValueChange={setModel} />
|
|
41
|
+
// another representation of the same job
|
|
42
|
+
<Select value={model} onValueChange={setModel} />
|
|
49
43
|
```
|
|
50
44
|
|
|
51
|
-
**
|
|
45
|
+
**Tokens and structure.** Use the existing app host, compiler, theme, shared components and Tailwind tokens. No custom-CSS/dynamic-class escape, hardcoded color/size exception or weakened shared rules. Keep surfaces flat and dense; group by proximity before adding borders or elevation. No nested cards or wrappers that merely label content again. Whitespace must support hierarchy, separation or comfortable use, not fill the page.
|
|
52
46
|
|
|
53
47
|
```tsx
|
|
54
|
-
//
|
|
55
|
-
<
|
|
56
|
-
//
|
|
57
|
-
<
|
|
48
|
+
// existing primitives and semantic tokens
|
|
49
|
+
<Empty><EmptyTitle>No runs yet</EmptyTitle><Button onClick={startRun}>Run</Button></Empty>
|
|
50
|
+
// parallel hand-built appearance
|
|
51
|
+
<div className="rounded-full bg-[#eef] px-[7px] text-[11px]">No runs yet</div>
|
|
58
52
|
```
|
|
59
53
|
|
|
60
|
-
**
|
|
54
|
+
**Typography.** Give headings, navigation, body text and data deliberate roles through the existing type scale, weights, line heights and readable line lengths. Use tabular numerals where values need comparison; let prose wrap without making controls inconsistent. Preserve established fonts and tokens rather than changing them for novelty. Do not substitute tiny labels, gratuitous capitals or isolated accent words for hierarchy.
|
|
61
55
|
|
|
62
|
-
|
|
63
|
-
// good
|
|
64
|
-
<TableRow data-state={selected ? 'selected' : undefined}>
|
|
65
|
-
// bad — "i'm not a fan that things like the empty star comes out on hover"
|
|
66
|
-
<Button className="opacity-0 group-hover:opacity-100" variant="ghost">
|
|
67
|
-
```
|
|
56
|
+
**Copy.** Name actions from the user's task, not implementation internals. Keep the same vocabulary across controls and confirmations. Use plain verbs and sentence case; labels describe outcomes, not decoration. Empty states offer a relevant next action; errors identify the failed action and available recovery. Reuse its existing control rather than repeating the action in text or adding another button; do not invent a recovery that cannot work.
|
|
68
57
|
|
|
69
|
-
|
|
58
|
+
| Weak | Useful |
|
|
59
|
+
| ---------------------- | ----------------------------------------------------------------- |
|
|
60
|
+
| `Submit` | `Save changes` → `Changes saved` |
|
|
61
|
+
| `Something went wrong` | `Couldn't save.` beside the existing retry action; keep the draft |
|
|
62
|
+
| `No data` | `No notes yet` beside the existing create action |
|
|
70
63
|
|
|
71
|
-
**
|
|
64
|
+
**Alignment.** Give related headings, search, pinned rows, dividers and records one owning grid/inset. Headers and row contents share columns; action columns stay fixed when labels wrap or are absent. Use the same component/size variant for equivalent controls: icon box/stroke, hit target, control height, baseline and spacing. Pinned content stays outside the scroll area; borders do not jump while scrolling. Fix the owning layout/component, not per-instance offsets; matching boxes alone does not establish optical alignment or legibility. Trace the cause before applying these repairs:
|
|
72
65
|
|
|
73
|
-
|
|
66
|
+
| Defect | Check | Owning repair |
|
|
67
|
+
| ---------------------------------- | --------------------------------------------- | -------------------------------------------------------------------------------------------------- |
|
|
68
|
+
| Squashed icon/avatar | Fixed-size flex child shrinks | Preserve its canonical box with `shrink-0` |
|
|
69
|
+
| Trailing action disappears | Content track refuses to shrink | `min-w-0` on content; `shrink-0` on the control, or a shrinkable grid track |
|
|
70
|
+
| Wrapped rows look adrift | Center alignment against variable-height text | Consistent, deliberate top/baseline alignment |
|
|
71
|
+
| Identifiers look identical | Truncation hides the distinguishing segment | Preserve that segment; expose the full value through an existing keyboard/touch-accessible surface |
|
|
72
|
+
| Counts shift during updates | Proportional digits/inconsistent formatting | `tabular-nums` + consistent locale/units |
|
|
73
|
+
| Accents/tall scripts clip | Tight line height or text-box clipping | Fix the owning type-scale/overflow choice; don't shrink the font to fit |
|
|
74
|
+
| Missing metadata leaves separators | Empty optional slots still render | Omit orphaned separators/lines; reserve space only when the layout needs it |
|
|
74
75
|
|
|
75
|
-
**
|
|
76
|
+
**Interaction and state.** Controls remain discoverable, never hover-only. Use the canonical selected-row/active-tab treatment; do not add a second badge or check column that echoes it. Keep pending/error feedback beside its owner and preserve recoverable input. No redundant refresh, copied state or polling shortcut around the existing reactive integration.
|
|
76
77
|
|
|
77
|
-
|
|
78
|
+
**Motion.** Explain change without delaying input or moving content just for decoration. Reuse existing tokens/components; no new library for a fade or universal curve, duration, bounce or press scale.
|
|
78
79
|
|
|
79
|
-
|
|
80
|
+
| Interaction | Treatment |
|
|
81
|
+
| -------------------------------------- | -------------------------------------------------------------------------------------------------- |
|
|
82
|
+
| Typing, shortcuts, frequent navigation | Immediate response; no decorative movement |
|
|
83
|
+
| Trigger-anchored surface | Enter/exit from its trigger; a centered unanchored modal keeps a centered origin |
|
|
84
|
+
| Rapid toggle/reversal | Retarget from the visible state; no input lock, stale-target snap or wait for the prior transition |
|
|
85
|
+
| Drag/gesture | Follow the pointer; preserve the existing primitive's capture, cancellation and release behavior |
|
|
86
|
+
| Reduced motion | Preserve feedback through a suitable static or gentle non-spatial equivalent |
|
|
80
87
|
|
|
81
|
-
|
|
88
|
+
Use responsive entry easing and brief existing durations. Simple retargetable states suit transitions; gestures may need the existing spring. Prefer transform/opacity where appropriate, but CSS/WAAPI does not guarantee compositor execution. Profile observed performance problems, not every animation. Inspect rapid repetition/reversal and reduced motion, not only the final frame.
|
|
82
89
|
|
|
83
|
-
**
|
|
90
|
+
**Access and responsiveness.** Preserve the complete task on phones, not merely stacked desktop panes. Keep essential actions reachable with keyboard, touch and zoom. Use semantic structure, accessible names, visible focus, logical order and deliberate focus return. Inspect long/duplicate content, overflow, contrast and relevant failure/permission states. Do not remove an accessible affordance to satisfy visual minimalism.
|
|
84
91
|
|
|
85
|
-
|
|
92
|
+
| Mobile defect | Check |
|
|
93
|
+
| --------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
|
94
|
+
| Hover remains after tap | Capability-aware styles; inspect the framework's handling before adding another gate |
|
|
95
|
+
| Input zoom/wrong keyboard | Readable input sizing + correct input type/mode; never disable browser zoom |
|
|
96
|
+
| Bottom controls disappear | Real browser chrome, software keyboard and safe areas; appropriate supported viewport sizing/insets |
|
|
97
|
+
| Drag blocks scrolling/content can't be copied | Scope gesture/selection restrictions to their owner; preserve scrolling, zoom and selectable content |
|
|
86
98
|
|
|
87
|
-
|
|
99
|
+
Touch and mouse can coexist; do not infer capabilities from user-agent strings or viewport width. No blanket suppression of overscroll, selection or native feedback without a demonstrated need and working replacement. Emulation verifies selected layout cases, not every phone behavior: affected device-specific keyboard/viewport/gesture contracts need target-device proof before completion.
|
|
88
100
|
|
|
89
|
-
##
|
|
101
|
+
## Prototype
|
|
90
102
|
|
|
91
|
-
**
|
|
103
|
+
**Try the question.** Build one rough representative screen/interaction in the existing host with realistic data. Inspect enough to make the trial trustworthy, then let the user try it before merge-readiness work. Label stubs and limits; a conceptual sketch is sufficient only when execution is unnecessary, never proof of working software. Set a short first-preview budget; show a partial result/blocker rather than expanding unseen. No production completeness or polished gallery first.
|
|
92
104
|
|
|
93
|
-
**
|
|
105
|
+
**Use the findings.** For layout questions, compare hierarchy or interaction, not color-only variants. Add attempts only for unresolved uncertainty, feedback or an explicit request—no fixed count; a style question can compare style. Keep useful comparisons; discard/rebuild freely and combine compatible strengths, not incompatible mechanisms. Test the weakest assumption while it can change the choice. A settled direction needs no manufactured taste question or exploration round.
|
|
94
106
|
|
|
95
|
-
|
|
107
|
+
## Verify and show
|
|
96
108
|
|
|
97
|
-
**
|
|
109
|
+
**Inspect retained UI.** Open actual captures at the sizes needed for the task. Compare related edges, columns, baselines and equivalent controls across rows/screens—not just successful clicks. Use realistic long names/URLs, translated labels, Unicode, missing fields and relevant 0/1/many records at the actual container width. Respect schema/API limits; never add a validation limit to make layout pass. Mix cases across rows. Check selected/focus/error states and scrolling; a clean default row can hide misalignment. Inspect hierarchy, density, legibility and clipping. Exercise the relevant keyboard/touch, narrow/wide, zoom and focus-return journeys; run axe where applicable. A clean scan does not prove contrast or usability. Inspect meaningful video frames and sequence; a playable file is not proof of a good composition. Batch relevant inspection/repairs; recheck affected views rather than restart a whole audit after each micro-edit. Visible misalignment, inconsistent controls or redundant elements remain defects even when types/tests pass; fix them before claiming acceptance.
|
|
98
110
|
|
|
99
|
-
**
|
|
111
|
+
**Show the design.** Use inspected screenshots for layout and short video for behavior. Compare at consistent sizes with states needed for the decision; no variant switcher just to see the options. Prefer the existing app-owned preview/element feedback. A visual must add spatial or interactive information, not reproduce prose, a file list or a fake runtime screenshot. Link useful existing evidence rather than render another explanation page. Use the user's theme and verify the actual browser color preference. Keep temporary source/services in ignored scratch, preserve useful media and retire settled attempts; persistent previews are opt-in.
|
|
100
112
|
|
|
101
|
-
|
|
113
|
+
**Review surfaces.** Open on the outcome the user needs to judge, with technical detail available on demand. Group by changed behavior, not files or agent stages. A selected example, diagram or diff must reveal its scope; never make curated evidence look like complete coverage. Do not build a custom review dashboard when existing captures and the PR can answer the question. A design-skill edit is not a product-screen change.
|
|
102
114
|
|
|
103
|
-
|
|
115
|
+
## Image assets
|
|
104
116
|
|
|
105
|
-
|
|
117
|
+
Use image generation only for a needed illustrative asset, atmosphere, texture or icon exploration—not ordinary text, exact diagrams or fake working UI. Use the native tool exposed by the harness; no invented invocation or replacement API client when unavailable. Report missing capability honestly.
|
|
106
118
|
|
|
107
|
-
|
|
119
|
+
Describe purpose, placement, composition, palette and intended size/aspect ratio. For edits, supply the real source and state what remains unchanged. Generate only what the chosen direction needs; prefer existing icons/CSS when they express the job precisely. Inspect the returned asset at its intended size for crop, text/edge artifacts, contrast and fit. Preserve useful originals and identify generated assets as such; they are not runtime/browser proof. Keep only used assets in product source.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: engineering
|
|
3
|
-
description: '
|
|
3
|
+
description: 'TypeScript and Effect code conventions paired with shared lint. Use when designing, implementing or reviewing product/test code, including prototypes.'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
**Scope.** Apply these rules and repository-specific CODING_STANDARDS.md.
|
|
6
|
+
**Scope.** Apply these rules and repository-specific CODING_STANDARDS.md.
|
|
7
7
|
|
|
8
|
-
**
|
|
8
|
+
**Dependency APIs.** Match manifest/lock versions. Inspect the relevant cloned upstream implementation, types and nearest real usage before inventing an API wrapper or workaround; never node_modules. Follow only the paths needed to settle the question, with no dependency install/build just to read source. Library APIs do not define coding policy.
|
|
9
9
|
|
|
10
10
|
**Diagnostics.** Use the shown idiom, not casts, weakened rules or a rewrite trading one diagnostic for another.
|
|
11
11
|
|
|
@@ -13,16 +13,18 @@ description: 'Engineering rules for product code, paired with the shared lint. U
|
|
|
13
13
|
|
|
14
14
|
**Clarity.** Plain, explicit, idiomatic code, names and files; read top to bottom without commentary or cleverness.
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
**Experiments.** Write with these idioms; reserve the full refactor/cleanup pass for retained delivery. Keep disposable source isolated and shared rules intact.
|
|
17
|
+
|
|
18
|
+
- **Requested only.** Check whether existing code, an installed module or a platform capability already satisfies the contract before writing more. Implement only the missing behavior; shorter code never justifies dropping requested behavior, safety or accessibility. Touch only what the request needs; unrelated improvements are reported. A refactor or cleanup request asks for depth instead: every rule applies to every line of the owned files.
|
|
17
19
|
- **Inward layers.** Domain and service code never import HTTP, RPC, or other transport types.
|
|
18
|
-
- **Building blocks.** Offer services, Layers
|
|
20
|
+
- **Building blocks.** For a consequential new interface, start with a realistic caller operation and its ordering, failure and lifetime obligations; hide useful complexity, not forwarded names. Offer services, Layers and functions the caller composes like any Effect module, never a bundle such as `serve(app)`. No mandatory architecture document or alternatives round.
|
|
19
21
|
- **No machinery.** Behave correctly instead of building hooks, guards, or generators to enforce behavior.
|
|
20
22
|
- **Behavior kept.** A refactor keeps behavior at every usage point. Two differences are accepted and reported with the change: one no usage point observes that makes the code simpler, and one a rule here causes, such as an error keeping its cause or sorted keys, unless a usage point parses it; printed diagnostics are not a contract. Any other behavior change is the user's decision, except a reachable bug's fix.
|
|
21
|
-
- **Reachable bugs.** A bug a real input from a usage point triggers is fixed and reported, with every consumer of the changed output checked. Handling for input no caller produces is deleted; anything that looks intentional or that other code relies on is kept and reported as possibly intentional.
|
|
22
|
-
- **Performance.** Improve performance in the touched code where you know how; measure on a realistic input only when a change claims speed or keeps a slower-looking form. Diagnose at the layer that owns the symptom: distributed spans for RPC/backend dependencies, React commits for rerenders, a browser performance profile for main-thread work. Correlate one real action before changing code, then repeat it on the fix; neither a screenshot nor a service name proves latency or trace coverage.
|
|
23
|
+
- **Reachable bugs.** A bug a real input from a usage point triggers is fixed and reported, with every consumer of the changed output checked. For hard bugs, use one symptom-specific replay and a prediction per probe; compare known-good/broken states when useful. Remove edits motivated by disproven hypotheses and check the original scenario after reducing a reproduction. Handling for input no caller produces is deleted; anything that looks intentional or that other code relies on is kept and reported as possibly intentional.
|
|
24
|
+
- **Performance.** Improve performance in the touched code where you know how; measure on a realistic input only when a change claims speed or keeps a slower-looking form. Diagnose at the layer that owns the symptom: distributed spans for RPC/backend dependencies, React commits for rerenders, a browser performance profile for main-thread work. Correlate one real action before changing code, then repeat it on the fix; neither a screenshot nor a service name proves latency or trace coverage. Time correct completed work under equivalent workloads/configuration; distinguish setup/cache/profiling cost and report run count/variation. Differences within noise are unproved, not speedups. Use existing instrumentation; profiling is not a ritual on every change.
|
|
23
25
|
- **Type safety.** A fix never weakens types to make a symptom go away: no widened or erased type, cast, dropped generic, or loosened exported type.
|
|
24
26
|
- **Domain.** Implement the definition the domain uses, such as a cycle for recursion, never the nearest syntactic proxy.
|
|
25
|
-
- **Reread.** Before
|
|
27
|
+
- **Reread.** Before delivering retained code, reread the diff and delete every line the outcome does not require.
|
|
26
28
|
|
|
27
29
|
**Happy path.** Failures flow through the error channel: no catch, retry, fallback, backup, or defensive check unless the request or an existing contract needs it.
|
|
28
30
|
|
|
@@ -52,7 +54,7 @@ Math.floor(Duration.toDays(DateTime.distance(oldest.timestamp, newest.timestamp)
|
|
|
52
54
|
import {glob} from 'glob' // a dependency Effect's FileSystem replaces
|
|
53
55
|
```
|
|
54
56
|
|
|
55
|
-
**Whole values.** Pass whole values or spread them. No parameter/callback/loop destructuring except useState and component ref; project fields only to exclude data the recipient must not receive.
|
|
57
|
+
**Whole values.** Pass whole values or spread them. No parameter/callback/loop destructuring except labeled tuple parameters, useState and component ref; project fields only to exclude data the recipient must not receive.
|
|
56
58
|
|
|
57
59
|
```ts
|
|
58
60
|
// good
|
|
@@ -176,7 +178,7 @@ const parsed = Option.getOrUndefined(Number.parse(event.target.value))
|
|
|
176
178
|
|
|
177
179
|
**Handlers.** A handler used once is an arrow in place; a handler used twice is a `function`.
|
|
178
180
|
|
|
179
|
-
**Pairs.**
|
|
181
|
+
**Pairs.** Positional tuples stay whole, read by index. Meaningful named bindings such as React state/setter pairs and explicitly labeled tuple parameters are useful exceptions.
|
|
180
182
|
|
|
181
183
|
**Accumulators.** No `let`: a fold or an Effect collection operation that says what it selects.
|
|
182
184
|
|
|
@@ -223,7 +225,7 @@ process.env.HOST ?? '0.0.0.0'
|
|
|
223
225
|
|
|
224
226
|
**Pipe.** A standalone `pipe`, nested at most three deep; never the `.pipe` method.
|
|
225
227
|
|
|
226
|
-
**Failures.**
|
|
228
|
+
**Failures.** Yield a yieldable domain error directly in a generator: `return yield* E.make(...)`. Keep `Effect.fail(error)` when the error is not yieldable; outside a generator, use `Effect.fail`. Do not infer yieldability from factory-call syntax.
|
|
227
229
|
|
|
228
230
|
```ts
|
|
229
231
|
// good
|
|
@@ -240,7 +242,7 @@ const text = Effect.fn('Ai.text')(function* (response: AiResponse) {
|
|
|
240
242
|
})
|
|
241
243
|
```
|
|
242
244
|
|
|
243
|
-
**Casts.** No `as`, `any
|
|
245
|
+
**Casts.** No `as`, `any` or erasing a known shape into `unknown`; `satisfies` and `as const` are the only assertions. Genuinely opaque SDK/framework inputs remain `unknown` until narrowed or decoded at their boundary—do not fabricate a shape to avoid that type. Decode incoming protocol data once with Schema before use.
|
|
244
246
|
|
|
245
247
|
```ts
|
|
246
248
|
// good
|
|
@@ -248,7 +250,7 @@ const params = Schema.decodeUnknownOption(Schema.Record(Schema.String, Schema.Un
|
|
|
248
250
|
return Effect.succeed([{text: part.text, type: 'text'} satisfies TextContent])
|
|
249
251
|
// bad — a cast satisfies one rule and the next flags it
|
|
250
252
|
const params = part.params as Record<string, unknown>
|
|
251
|
-
|
|
253
|
+
const params: unknown = part.params // erases an already-known shape
|
|
252
254
|
```
|
|
253
255
|
|
|
254
256
|
**Decoders.** `Schema.decodeUnknownEffect` only for `unknown` input; typed input uses `Schema.decodeEffect`, and JSON text goes through `Schema.fromJsonString`.
|
|
@@ -279,7 +281,7 @@ import {type Plugin, createServer} from 'vite'
|
|
|
279
281
|
export const EntryKind = Schema.Literals(['income', 'expense']) // no importer
|
|
280
282
|
```
|
|
281
283
|
|
|
282
|
-
**Readonly.**
|
|
284
|
+
**Readonly.** Required on service-shape properties. Allowed on class members and handwritten `Schema.suspend` cycle types; otherwise forbidden.
|
|
283
285
|
|
|
284
286
|
**Disables.** Fix the idiom or reproduce a rule defect. Inline disables require an unavoidable conflict and -- reason; never disable ordering or weaken file configuration. Report dependency-only diagnostics; rename filename violations.
|
|
285
287
|
|
|
@@ -377,7 +379,7 @@ static readonly layer = Layer.effect(Ledger, makeLedger) // a make with one call
|
|
|
377
379
|
query(sql: string): Effect.Effect<Rows> // a method signature instead of a readonly property
|
|
378
380
|
```
|
|
379
381
|
|
|
380
|
-
**Primitives.** Use Effect's primitives directly; rpcs and services already trace, and one scope owns each lifetime.
|
|
382
|
+
**Primitives.** Use Effect's primitives directly; rpcs and services already trace, and one scope owns each lifetime. Compose established independent I/O with Effect.all and explicit appropriate concurrency; keep order-dependent, transactional or resource-conflicting work sequential.
|
|
381
383
|
|
|
382
384
|
```ts
|
|
383
385
|
// good
|
|
@@ -442,7 +444,7 @@ const label = MutableRef.make<string>('') // repeats the inferred type
|
|
|
442
444
|
export type SandboxLike = {id: string; renew: (seconds: number) => Promise<unknown>} // a copy of an inferred shape
|
|
443
445
|
```
|
|
444
446
|
|
|
445
|
-
**Schema pairs.** Type pair immediately before every schema. Structs use Schema.Struct, never Schema.Class; reuse fields by spreading.
|
|
447
|
+
**Schema pairs.** Type pair immediately before every schema. Structs use Schema.Struct, never Schema.Class; reuse fields by spreading. Use Schema.optionalKey for absent fields; Schema.optional allows explicit undefined too, so reserve it for that contract.
|
|
446
448
|
|
|
447
449
|
```ts
|
|
448
450
|
// good
|
|
@@ -460,6 +462,18 @@ function cursorFromBigInt(value: bigint) {
|
|
|
460
462
|
payload: Schema.Struct(pipe(PortfolioVisitor.fields, Struct.pick(['color', 'id', 'name', 'x', 'y']))) // the schema itself fits
|
|
461
463
|
```
|
|
462
464
|
|
|
465
|
+
**Construction.** `schema.make` applies defaults and validates synchronously, throwing on failure; use it for trusted construction. For reachable fallible construction with defaults, use `schema.makeEffect` and map its `SchemaIssue.Issue` to the owning domain error. Effect decoders report `SchemaError`; use the correct error contract and do not revalidate already-narrowed data.
|
|
466
|
+
|
|
467
|
+
```ts
|
|
468
|
+
// good — reachable constructor failure stays in the error channel
|
|
469
|
+
pipe(
|
|
470
|
+
Name.makeEffect(input),
|
|
471
|
+
Effect.mapError(cause => NameError.make({cause, message: 'Invalid name'}))
|
|
472
|
+
)
|
|
473
|
+
// bad — input can fail a refinement even though its TypeScript type is string
|
|
474
|
+
Name.make(input)
|
|
475
|
+
```
|
|
476
|
+
|
|
463
477
|
**Cycles.** Handwrite each recursive-cycle type; annotate only Schema.suspend thunks. Noncycle types stay inferred; unions follow members.
|
|
464
478
|
|
|
465
479
|
```ts
|
|
@@ -557,6 +571,6 @@ const state = useState(false)
|
|
|
557
571
|
```
|
|
558
572
|
|
|
559
573
|
- **Pure render.** Derive display values from props and subscribed state during render; never mutate them or perform an RPC, write or subscription there. Event handlers own user-triggered actions; Effects synchronize external systems, not copied state or calculations. Reuse the existing router/atom integration rather than fetching in a new Effect.
|
|
560
|
-
- **State identity.** Keep one owner for each value, stable domain keys for list items and deliberate reset boundaries. Compiler memoization is a performance optimization, never a correctness or lifetime guarantee. Do not add manual memoization, compiler escape directives or weakened hook rules to hide an ownership problem.
|
|
574
|
+
- **State identity.** Keep one owner for each value, stable domain keys for list items and deliberate reset boundaries. For an observed rerender problem, derive the needed value in the existing atom graph instead of subscribing to frequently changing unrelated state. Compiler memoization is a performance optimization, never a correctness or lifetime guarantee. Do not add manual memoization, compiler escape directives or weakened hook rules to hide an ownership problem.
|
|
561
575
|
|
|
562
576
|
Testing judgment, fixtures, assertions and verification belong to the testing skill; these engineering rules still apply to test code.
|
package/skills/testing/SKILL.md
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: testing
|
|
3
|
-
description: '
|
|
3
|
+
description: 'Select and run black-box behavior proof for Effect apps/tools. Use when choosing, writing, reviewing or running tests; apply engineering to test code and keep exploratory proof question-sized.'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Testing
|
|
7
7
|
|
|
8
|
-
**Scope.** Guard owned behavior, not coverage.
|
|
8
|
+
**Scope.** Guard owned behavior, not coverage.
|
|
9
|
+
|
|
10
|
+
**Timing.** An experiment needs trustworthy proof of its question, not comprehensive release coverage. Final acceptance applies to retained delivery; label stubbed/unproved behavior.
|
|
9
11
|
|
|
10
12
|
- **Worth it.** Test owned branching, computation, parsing/state transitions a plausible regression breaks; add missing valuable coverage before refactoring.
|
|
11
13
|
- **Seam.** Test the exported layer, service, or function of a package or an app's service as a black box, so rewriting the implementation leaves every test green; the browser proves UI components.
|
|
@@ -38,7 +40,7 @@ assert.strictEqual(Array.flatMap(attioProvider.groups, group => group.actions).l
|
|
|
38
40
|
|
|
39
41
|
## Choose cases
|
|
40
42
|
|
|
41
|
-
- **Contract.** Reachable input → observable output; a plausible wrong implementation must fail. Use the nearest public seam, not private helpers/new harnesses.
|
|
43
|
+
- **Contract.** Reachable input → observable output; a plausible wrong implementation must fail. Use the nearest public seam, not private helpers/new harnesses. When behavior is uncertain, settle one reachable case with a focused check and minimal implementation, then choose the next case from the findings; do not batch tests for an imagined implementation or require universal TDD.
|
|
42
44
|
- **Branches.** Exercise reachable empty/duplicate/concurrent/dependency-failure distinctions; no Cartesian product or random/repeated happy paths without a hypothesis.
|
|
43
45
|
- **Failure.** Effect.flip for expected failure; Effect.exit for both outcomes. Assert domain identity and preserved state/resources: no partial write, lost value, extra retry or leak. Wording/rejection alone is insufficient; no incidental order assertions.
|
|
44
46
|
- **Control.** Run against the unfixed implementation: the intended assertion fails, not setup/import/compilation. For refactoring, add missing coverage on old behavior first. Keep durable regressions, remove settled probes.
|
|
@@ -60,4 +62,4 @@ assert.strictEqual(Array.flatMap(attioProvider.groups, group => group.actions).l
|
|
|
60
62
|
- **Inventory.** Collect independent failures once instead of cancelling the suite at the first failure. Distinguish separate cases from repeated observations. A timeout receives one isolated rerun of that same test; after a genuine pass, stop investigating it. Do not call a bypassed selector or a skipped operation flaky.
|
|
61
63
|
- **Evidence.** Preserve the first failure, exact command, execution count and exit status. Separate cause from hypothesis, product defect from setup failure, and a skipped criterion from a passing one. A source read or overall green command never substitutes for a missing behavior check.
|
|
62
64
|
- **Stable inputs.** No writer changes a running check's source, dependencies, configuration or build inputs. A relevant edit invalidates its prior result; rerun affected criteria and reuse unaffected evidence. Independent research or browser journeys may continue without mutating those inputs.
|
|
63
|
-
- **Final proof.** Reconcile every required behavior on the final code. Screenshots/video prove visible outcomes; public-seam execution proves nonvisual behavior; fresh agents
|
|
65
|
+
- **Final proof.** Reconcile every required behavior on the final code. Screenshots/video prove visible outcomes; public-seam execution proves nonvisual behavior; use fresh agents for changed instructions only when they can settle a consequential unresolved uncertainty, not routine wording edits. Keep the result and useful evidence, not disposable drivers, fixture installs, reports or preview services.
|