@shivaedev/quality 0.27.0 → 0.28.0

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.
Files changed (129) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +26 -1
  3. package/dist/biome/run.d.ts.map +1 -1
  4. package/dist/biome/run.js +15 -2
  5. package/dist/biome/run.js.map +1 -1
  6. package/dist/cli/args.d.ts +3 -1
  7. package/dist/cli/args.d.ts.map +1 -1
  8. package/dist/cli/args.js +3 -0
  9. package/dist/cli/args.js.map +1 -1
  10. package/dist/cli/messages.d.ts +1 -1
  11. package/dist/cli/messages.d.ts.map +1 -1
  12. package/dist/cli/messages.js +3 -1
  13. package/dist/cli/messages.js.map +1 -1
  14. package/dist/cli/program.d.ts.map +1 -1
  15. package/dist/cli/program.js +3 -0
  16. package/dist/cli/program.js.map +1 -1
  17. package/dist/hooks/install.d.ts.map +1 -1
  18. package/dist/hooks/install.js +9 -8
  19. package/dist/hooks/install.js.map +1 -1
  20. package/dist/hooks/shim.d.ts.map +1 -1
  21. package/dist/hooks/shim.js +11 -1
  22. package/dist/hooks/shim.js.map +1 -1
  23. package/dist/hooks/status.d.ts +6 -0
  24. package/dist/hooks/status.d.ts.map +1 -0
  25. package/dist/hooks/status.js +45 -0
  26. package/dist/hooks/status.js.map +1 -0
  27. package/dist/hooks/uninstall.d.ts.map +1 -1
  28. package/dist/hooks/uninstall.js +6 -0
  29. package/dist/hooks/uninstall.js.map +1 -1
  30. package/dist/inventory/collect.d.ts.map +1 -1
  31. package/dist/inventory/collect.js +3 -0
  32. package/dist/inventory/collect.js.map +1 -1
  33. package/dist/inventory/shadcnAlias.d.ts +2 -0
  34. package/dist/inventory/shadcnAlias.d.ts.map +1 -0
  35. package/dist/inventory/shadcnAlias.js +73 -0
  36. package/dist/inventory/shadcnAlias.js.map +1 -0
  37. package/dist/inventory/shadcnFolders.d.ts +4 -0
  38. package/dist/inventory/shadcnFolders.d.ts.map +1 -0
  39. package/dist/inventory/shadcnFolders.js +25 -0
  40. package/dist/inventory/shadcnFolders.js.map +1 -0
  41. package/dist/naming/migrationFile.d.ts +4 -0
  42. package/dist/naming/migrationFile.d.ts.map +1 -0
  43. package/dist/naming/migrationFile.js +8 -0
  44. package/dist/naming/migrationFile.js.map +1 -0
  45. package/dist/rules/builtIn.d.ts +7 -1
  46. package/dist/rules/builtIn.d.ts.map +1 -1
  47. package/dist/rules/builtIn.js +16 -0
  48. package/dist/rules/builtIn.js.map +1 -1
  49. package/dist/rules/files/folderNames.d.ts.map +1 -1
  50. package/dist/rules/files/folderNames.js +1 -2
  51. package/dist/rules/files/folderNames.js.map +1 -1
  52. package/dist/rules/files/namedAfterExport.d.ts.map +1 -1
  53. package/dist/rules/files/namedAfterExport.js +4 -1
  54. package/dist/rules/files/namedAfterExport.js.map +1 -1
  55. package/dist/rules/files/otherNames.js +1 -1
  56. package/dist/rules/files/otherNames.js.map +1 -1
  57. package/dist/rules/stories/authSql.d.ts +4 -0
  58. package/dist/rules/stories/authSql.d.ts.map +1 -0
  59. package/dist/rules/stories/authSql.js +102 -0
  60. package/dist/rules/stories/authSql.js.map +1 -0
  61. package/dist/rules/stories/httpStatus.d.ts +4 -0
  62. package/dist/rules/stories/httpStatus.d.ts.map +1 -0
  63. package/dist/rules/stories/httpStatus.js +47 -0
  64. package/dist/rules/stories/httpStatus.js.map +1 -0
  65. package/dist/rules/stories/matchesAny.d.ts +2 -0
  66. package/dist/rules/stories/matchesAny.d.ts.map +1 -0
  67. package/dist/rules/stories/matchesAny.js +5 -0
  68. package/dist/rules/stories/matchesAny.js.map +1 -0
  69. package/dist/rules/stories/namedValues.d.ts +2 -0
  70. package/dist/rules/stories/namedValues.d.ts.map +1 -0
  71. package/dist/rules/stories/namedValues.js +15 -0
  72. package/dist/rules/stories/namedValues.js.map +1 -0
  73. package/dist/rules/stories/noExpect.d.ts +2 -0
  74. package/dist/rules/stories/noExpect.d.ts.map +1 -0
  75. package/dist/rules/stories/noExpect.js +33 -0
  76. package/dist/rules/stories/noExpect.js.map +1 -0
  77. package/dist/rules/stories/noLimitImports.d.ts +2 -0
  78. package/dist/rules/stories/noLimitImports.d.ts.map +1 -0
  79. package/dist/rules/stories/noLimitImports.js +66 -0
  80. package/dist/rules/stories/noLimitImports.js.map +1 -0
  81. package/dist/rules/stories/queueSql.d.ts +2 -0
  82. package/dist/rules/stories/queueSql.d.ts.map +1 -0
  83. package/dist/rules/stories/queueSql.js +15 -0
  84. package/dist/rules/stories/queueSql.js.map +1 -0
  85. package/dist/rules/stories/specHelpers.d.ts +2 -0
  86. package/dist/rules/stories/specHelpers.d.ts.map +1 -0
  87. package/dist/rules/stories/specHelpers.js +9 -0
  88. package/dist/rules/stories/specHelpers.js.map +1 -0
  89. package/dist/rules/stories/sqlText.d.ts +8 -0
  90. package/dist/rules/stories/sqlText.d.ts.map +1 -0
  91. package/dist/rules/stories/sqlText.js +86 -0
  92. package/dist/rules/stories/sqlText.js.map +1 -0
  93. package/dist/rules/stories/supportImports.d.ts +4 -0
  94. package/dist/rules/stories/supportImports.d.ts.map +1 -0
  95. package/dist/rules/stories/supportImports.js +28 -0
  96. package/dist/rules/stories/supportImports.js.map +1 -0
  97. package/dist/rules/stories/syntax.d.ts +11 -0
  98. package/dist/rules/stories/syntax.d.ts.map +1 -0
  99. package/dist/rules/stories/syntax.js +71 -0
  100. package/dist/rules/stories/syntax.js.map +1 -0
  101. package/docs/debt-and-hooks.md +4 -2
  102. package/package.json +5 -2
  103. package/src/biome/run.ts +16 -2
  104. package/src/cli/args.ts +4 -0
  105. package/src/cli/messages.ts +3 -1
  106. package/src/cli/program.ts +3 -0
  107. package/src/hooks/install.ts +11 -12
  108. package/src/hooks/shim.ts +11 -1
  109. package/src/hooks/status.ts +50 -0
  110. package/src/hooks/uninstall.ts +6 -0
  111. package/src/inventory/collect.ts +3 -0
  112. package/src/inventory/shadcnAlias.ts +88 -0
  113. package/src/inventory/shadcnFolders.ts +30 -0
  114. package/src/naming/migrationFile.ts +9 -0
  115. package/src/rules/builtIn.ts +16 -0
  116. package/src/rules/files/folderNames.ts +1 -3
  117. package/src/rules/files/namedAfterExport.ts +4 -1
  118. package/src/rules/files/otherNames.ts +1 -1
  119. package/src/rules/stories/authSql.ts +117 -0
  120. package/src/rules/stories/httpStatus.ts +56 -0
  121. package/src/rules/stories/matchesAny.ts +5 -0
  122. package/src/rules/stories/namedValues.ts +21 -0
  123. package/src/rules/stories/noExpect.ts +39 -0
  124. package/src/rules/stories/noLimitImports.ts +80 -0
  125. package/src/rules/stories/queueSql.ts +18 -0
  126. package/src/rules/stories/specHelpers.ts +10 -0
  127. package/src/rules/stories/sqlText.ts +100 -0
  128. package/src/rules/stories/supportImports.ts +31 -0
  129. package/src/rules/stories/syntax.ts +80 -0
@@ -0,0 +1,33 @@
1
+ import ts from "typescript";
2
+ import { isTestSupport } from "#naming/testName.ts";
3
+ import { defineRule } from "#rule.ts";
4
+ import { memberName, nodesWhere, syntaxFindings, unwrapped } from "./syntax.js";
5
+ const MESSAGE = "Calls expect in test-support. Return the engine's observed state from the verb and put the expectation in the spec; throw if an action cannot run.";
6
+ function assertionReference(expression, names) {
7
+ const node = unwrapped(expression);
8
+ if (ts.isIdentifier(node)) {
9
+ return names.has(node.text);
10
+ }
11
+ return (memberName(node) === "expect"
12
+ || ((ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) && assertionReference(node.expression, names)));
13
+ }
14
+ function assertionsIn(source) {
15
+ const names = new Set(["expect"]);
16
+ for (const statement of source.statements) {
17
+ const bindings = ts.isImportDeclaration(statement) ? statement.importClause?.namedBindings : undefined;
18
+ if (bindings !== undefined && ts.isNamedImports(bindings)) {
19
+ for (const element of bindings.elements) {
20
+ if ((element.propertyName ?? element.name).text === "expect") {
21
+ names.add(element.name.text);
22
+ }
23
+ }
24
+ }
25
+ }
26
+ return nodesWhere(source, (node) => ts.isCallExpression(node) && assertionReference(node.expression, names));
27
+ }
28
+ export const noExpect = defineRule({
29
+ check: ({ sources }) => sources.filter((file) => isTestSupport(file.path)).flatMap((file) => syntaxFindings(file, assertionsIn, MESSAGE)),
30
+ description: "Test-support performs actions and exposes observations; specs own expectations.",
31
+ id: "tests/no-expect-in-support",
32
+ });
33
+ //# sourceMappingURL=noExpect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"noExpect.js","sourceRoot":"","sources":["../../../src/rules/stories/noExpect.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,YAAY,CAAC;AAC5B,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAEhF,MAAM,OAAO,GACZ,oJAAoJ,CAAC;AAEtJ,SAAS,kBAAkB,CAAC,UAAyB,EAAE,KAA0B;IAChF,MAAM,IAAI,GAAG,SAAS,CAAC,UAAU,CAAC,CAAC;IACnC,IAAI,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC;QAC3B,OAAO,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC;IACD,OAAO,CACN,UAAU,CAAC,IAAI,CAAC,KAAK,QAAQ;WAC1B,CAAC,CAAC,EAAE,CAAC,0BAA0B,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,yBAAyB,CAAC,IAAI,CAAC,CAAC,IAAI,kBAAkB,CAAC,IAAI,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC,CAC9H,CAAC;AACH,CAAC;AAED,SAAS,YAAY,CAAC,MAAqB;IAC1C,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC;IAClC,KAAK,MAAM,SAAS,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC;QAC3C,MAAM,QAAQ,GAAG,EAAE,CAAC,mBAAmB,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,YAAY,EAAE,aAAa,CAAC,CAAC,CAAC,SAAS,CAAC;QACvG,IAAI,QAAQ,KAAK,SAAS,IAAI,EAAE,CAAC,cAAc,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC3D,KAAK,MAAM,OAAO,IAAI,QAAQ,CAAC,QAAQ,EAAE,CAAC;gBACzC,IAAI,CAAC,OAAO,CAAC,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;oBAC9D,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBAC9B,CAAC;YACF,CAAC;QACF,CAAC;IACF,CAAC;IACD,OAAO,UAAU,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC,gBAAgB,CAAC,IAAI,CAAC,IAAI,kBAAkB,CAAC,IAAI,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC,CAAC;AAC9G,CAAC;AAED,MAAM,CAAC,MAAM,QAAQ,GAAG,UAAU,CAAC;IAClC,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,cAAc,CAAC,IAAI,EAAE,YAAY,EAAE,OAAO,CAAC,CAAC;IACzI,WAAW,EAAE,iFAAiF;IAC9F,EAAE,EAAE,4BAA4B;CAChC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare const noLimitImports: import("#rule.ts").Rule<"tests/no-limit-imports", undefined>;
2
+ //# sourceMappingURL=noLimitImports.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"noLimitImports.d.ts","sourceRoot":"","sources":["../../../src/rules/stories/noLimitImports.ts"],"names":[],"mappings":"AA2EA,eAAO,MAAM,cAAc,8DAIzB,CAAC"}
@@ -0,0 +1,66 @@
1
+ import ts from "typescript";
2
+ import { defineRule } from "#rule.ts";
3
+ import { isSpec, memberName, nodesWhere, syntaxFindings, unwrapped } from "./syntax.js";
4
+ const MESSAGE = "Imports a MAX_* constant from a limits module. Tell a story about the observed behavior instead of asserting a copy of the implementation's cap.";
5
+ const LIMITS_MODULE = /(?:^|[/#])limits(?:\.[cm]?[jt]s)?(?:\/|$)/u;
6
+ const MAX_CONSTANT = /^MAX_/u;
7
+ function literalLimitsImport(expression) {
8
+ const node = unwrapped(expression);
9
+ if (ts.isAwaitExpression(node)) {
10
+ return literalLimitsImport(node.expression);
11
+ }
12
+ return (ts.isCallExpression(node)
13
+ && node.expression.kind === ts.SyntaxKind.ImportKeyword
14
+ && node.arguments[0] !== undefined
15
+ && ts.isStringLiteralLike(node.arguments[0])
16
+ && LIMITS_MODULE.test(node.arguments[0].text));
17
+ }
18
+ function capBinding(binding) {
19
+ if (binding.dotDotDotToken !== undefined) {
20
+ return false;
21
+ }
22
+ const name = binding.propertyName ?? binding.name;
23
+ if (ts.isComputedPropertyName(name)) {
24
+ return ts.isStringLiteralLike(name.expression) && MAX_CONSTANT.test(name.expression.text);
25
+ }
26
+ return (ts.isIdentifier(name) || ts.isStringLiteralLike(name)) && MAX_CONSTANT.test(name.text);
27
+ }
28
+ function capImports(source) {
29
+ const namespaces = new Set();
30
+ const imports = source.statements.flatMap((statement) => {
31
+ if (!(ts.isImportDeclaration(statement) && ts.isStringLiteralLike(statement.moduleSpecifier) && LIMITS_MODULE.test(statement.moduleSpecifier.text))) {
32
+ return [];
33
+ }
34
+ const bindings = statement.importClause?.namedBindings;
35
+ if (bindings !== undefined && ts.isNamespaceImport(bindings)) {
36
+ namespaces.add(bindings.name.text);
37
+ }
38
+ return bindings !== undefined && ts.isNamedImports(bindings)
39
+ ? bindings.elements.filter((element) => MAX_CONSTANT.test((element.propertyName ?? element.name).text))
40
+ : [];
41
+ });
42
+ const dynamic = nodesWhere(source, ts.isVariableDeclaration).flatMap((node) => {
43
+ if (!ts.isVariableDeclaration(node) || node.initializer === undefined || !literalLimitsImport(node.initializer)) {
44
+ return [];
45
+ }
46
+ if (ts.isIdentifier(node.name)) {
47
+ namespaces.add(node.name.text);
48
+ return [];
49
+ }
50
+ return ts.isObjectBindingPattern(node.name) ? node.name.elements.filter(capBinding) : [];
51
+ });
52
+ return [
53
+ ...imports,
54
+ ...dynamic,
55
+ ...nodesWhere(source, (node) => (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node))
56
+ && ts.isIdentifier(node.expression)
57
+ && namespaces.has(node.expression.text)
58
+ && MAX_CONSTANT.test(memberName(node) ?? "")),
59
+ ];
60
+ }
61
+ export const noLimitImports = defineRule({
62
+ check: ({ sources }) => sources.filter((file) => isSpec(file.path)).flatMap((file) => syntaxFindings(file, capImports, MESSAGE)),
63
+ description: "Stories prove behavior instead of importing implementation caps from limits modules.",
64
+ id: "tests/no-limit-imports",
65
+ });
66
+ //# sourceMappingURL=noLimitImports.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"noLimitImports.js","sourceRoot":"","sources":["../../../src/rules/stories/noLimitImports.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export declare const queueSql: import("#rule.ts").Rule<"boundaries/queue-sql", undefined>;
2
+ //# sourceMappingURL=queueSql.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queueSql.d.ts","sourceRoot":"","sources":["../../../src/rules/stories/queueSql.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,QAAQ,4DAYnB,CAAC"}
@@ -0,0 +1,15 @@
1
+ import { defineRule } from "#rule.ts";
2
+ import { sqlTexts } from "./sqlText.js";
3
+ const QUEUE_SCHEMA = /\bpgboss\b"?\s*\./iu;
4
+ export const queueSql = defineRule({
5
+ check: async (inputs) => (await sqlTexts(inputs))
6
+ .filter((query) => QUEUE_SCHEMA.test(query.text))
7
+ .map((query) => ({
8
+ file: query.file,
9
+ line: query.line,
10
+ message: "SQL mentions pgboss. Use the app's public queue definitions and let workers process jobs; apps never edit or purge queue tables, and there is no queue-purge API by design.",
11
+ })),
12
+ description: "Apps never read, edit or purge pg-boss's private queue schema.",
13
+ id: "boundaries/queue-sql",
14
+ });
15
+ //# sourceMappingURL=queueSql.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queueSql.js","sourceRoot":"","sources":["../../../src/rules/stories/queueSql.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAExC,MAAM,YAAY,GAAG,qBAAqB,CAAC;AAE3C,MAAM,CAAC,MAAM,QAAQ,GAAG,UAAU,CAAC;IAClC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,CACvB,CAAC,MAAM,QAAQ,CAAC,MAAM,CAAC,CAAC;SACtB,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;SAChD,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QAChB,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,OAAO,EACN,6KAA6K;KAC9K,CAAC,CAAC;IACL,WAAW,EAAE,gEAAgE;IAC7E,EAAE,EAAE,sBAAsB;CAC1B,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare const specHelpers: import("#rule.ts").Rule<"tests/spec-helpers", undefined>;
2
+ //# sourceMappingURL=specHelpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"specHelpers.d.ts","sourceRoot":"","sources":["../../../src/rules/stories/specHelpers.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,WAAW,0DAItB,CAAC"}
@@ -0,0 +1,9 @@
1
+ import { defineRule } from "#rule.ts";
2
+ import { isSpec, syntaxFindings, topLevelHelpers } from "./syntax.js";
3
+ const MESSAGE = "Declares a top-level helper in a spec. Move the action into the story kit's verbs or the setup into its traits in test-support/.";
4
+ export const specHelpers = defineRule({
5
+ check: ({ sources }) => sources.filter((file) => isSpec(file.path)).flatMap((file) => syntaxFindings(file, topLevelHelpers, MESSAGE)),
6
+ description: "Specs state expectations; story-kit traits and verbs in test-support own helpers.",
7
+ id: "tests/spec-helpers",
8
+ });
9
+ //# sourceMappingURL=specHelpers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"specHelpers.js","sourceRoot":"","sources":["../../../src/rules/stories/specHelpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEtE,MAAM,OAAO,GAAG,kIAAkI,CAAC;AAEnJ,MAAM,CAAC,MAAM,WAAW,GAAG,UAAU,CAAC;IACrC,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,cAAc,CAAC,IAAI,EAAE,eAAe,EAAE,OAAO,CAAC,CAAC;IACrI,WAAW,EAAE,mFAAmF;IAChG,EAAE,EAAE,oBAAoB;CACxB,CAAC,CAAC"}
@@ -0,0 +1,8 @@
1
+ import type { RuleInputs } from "#rule.ts";
2
+ export interface SqlText {
3
+ readonly file: string;
4
+ readonly line: number;
5
+ readonly text: string;
6
+ }
7
+ export declare function sqlTexts(inputs: RuleInputs): Promise<readonly SqlText[]>;
8
+ //# sourceMappingURL=sqlText.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sqlText.d.ts","sourceRoot":"","sources":["../../../src/rules/stories/sqlText.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAI3C,MAAM,WAAW,OAAO;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACtB;AAyED,wBAAsB,QAAQ,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,OAAO,EAAE,CAAC,CAiB9E"}
@@ -0,0 +1,86 @@
1
+ import ts from "typescript";
2
+ import { parse } from "#rules/syntax.ts";
3
+ import { unwrapped } from "./syntax.js";
4
+ const SQL_START = /^\s*(?:(?:--[^\n]*\n|\/\*[\s\S]*?\*\/)\s*)*(?:select|with|insert|update|delete|truncate|alter|drop|create|grant|revoke|comment)\b/iu;
5
+ const SQL_CALLEE = /^(?:sql|query|execute|exec|raw)(?:\.|$)|\.(?:sql|unsafe|query|execute|exec|raw)$/iu;
6
+ function interpolation(node) {
7
+ if (ts.isStringLiteralLike(node)) {
8
+ return node.text;
9
+ }
10
+ return ts.isCallExpression(node) && node.arguments.every(ts.isStringLiteralLike)
11
+ ? node.arguments
12
+ .filter(ts.isStringLiteralLike)
13
+ .map((argument) => argument.text)
14
+ .join(".")
15
+ : " ? ";
16
+ }
17
+ function templateText(node) {
18
+ return ts.isNoSubstitutionTemplateLiteral(node)
19
+ ? node.text
20
+ : node.head.text + node.templateSpans.map((span) => interpolation(span.expression) + span.literal.text).join("");
21
+ }
22
+ function textOf(expression) {
23
+ const node = unwrapped(expression);
24
+ if (ts.isStringLiteralLike(node)) {
25
+ return node.text;
26
+ }
27
+ if (ts.isTemplateExpression(node)) {
28
+ return templateText(node);
29
+ }
30
+ if (ts.isObjectLiteralExpression(node)) {
31
+ const query = node.properties.find((property) => ts.isPropertyAssignment(property) && property.name.getText() === "text");
32
+ return query !== undefined && ts.isPropertyAssignment(query) ? textOf(query.initializer) : undefined;
33
+ }
34
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.PlusToken) {
35
+ return `${textOf(node.left) ?? " ? "}${textOf(node.right) ?? " ? "}`;
36
+ }
37
+ return undefined;
38
+ }
39
+ function queryAt(node, source) {
40
+ let text;
41
+ let explicit = false;
42
+ if (ts.isTaggedTemplateExpression(node)) {
43
+ text = templateText(node.template);
44
+ explicit = SQL_CALLEE.test(node.tag.getText(source));
45
+ }
46
+ else if (ts.isCallExpression(node)) {
47
+ text = node.arguments[0] === undefined ? undefined : textOf(node.arguments[0]);
48
+ explicit = SQL_CALLEE.test(node.expression.getText(source));
49
+ }
50
+ else if (ts.isVariableDeclaration(node) && node.initializer !== undefined) {
51
+ text = textOf(node.initializer);
52
+ }
53
+ return text !== undefined && (explicit || SQL_START.test(text)) ? text : undefined;
54
+ }
55
+ function queriesIn(file, source) {
56
+ const queries = [];
57
+ function visit(node) {
58
+ const text = queryAt(node, source);
59
+ if (text !== undefined) {
60
+ queries.push({ file, line: source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1, text });
61
+ return;
62
+ }
63
+ if (!ts.isTaggedTemplateExpression(node)) {
64
+ ts.forEachChild(node, visit);
65
+ }
66
+ }
67
+ visit(source);
68
+ return queries;
69
+ }
70
+ export async function sqlTexts(inputs) {
71
+ const code = inputs.sources.flatMap((file) => {
72
+ const source = parse(file);
73
+ return source === undefined ? [] : queriesIn(file.path, source);
74
+ });
75
+ const sql = await Promise.all(inputs.files
76
+ .filter((file) => file.endsWith(".sql"))
77
+ .map(async (file) => {
78
+ const text = inputs.sources.find((source) => source.path === file)?.text ?? (await inputs.readText(file));
79
+ if (text === undefined) {
80
+ throw new Error(`Cannot read SQL file ${file}. Restore the file or repair its read permissions.`);
81
+ }
82
+ return { file, line: 1, text };
83
+ }));
84
+ return [...code, ...sql];
85
+ }
86
+ //# sourceMappingURL=sqlText.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sqlText.js","sourceRoot":"","sources":["../../../src/rules/stories/sqlText.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ export declare const supportImports: import("#rule.ts").Rule<"tests/support-imports", {
2
+ readonly modules?: readonly string[];
3
+ }>;
4
+ //# sourceMappingURL=supportImports.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"supportImports.d.ts","sourceRoot":"","sources":["../../../src/rules/stories/supportImports.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,cAAc;;EAoBzB,CAAC"}
@@ -0,0 +1,28 @@
1
+ import { Effect, Schema } from "effect";
2
+ import { importGraph } from "#imports/graph.ts";
3
+ import { isTestSupport } from "#naming/testName.ts";
4
+ import { defineRule } from "#rule.ts";
5
+ import { matchesAny } from "./matchesAny.js";
6
+ const SupportImportsOptions = Schema.Struct({
7
+ modules: Schema.Array(Schema.String).pipe(Schema.withDecodingDefaultKey(Effect.succeed([]))),
8
+ });
9
+ export const supportImports = defineRule({
10
+ check: async ({ options, ...inputs }) => {
11
+ if (!inputs.sources.some((file) => isTestSupport(file.path))) {
12
+ return [];
13
+ }
14
+ const graph = await importGraph(inputs);
15
+ return graph.edges
16
+ .filter((edge) => isTestSupport(edge.from) && edge.to.kind === "file" && !isTestSupport(edge.to.path) && !matchesAny(edge.to.path, options.modules))
17
+ .map((edge) => ({
18
+ file: edge.from,
19
+ line: edge.line,
20
+ message: `Test-support imports "${edge.specifier}" outside its allowed public surface. Drive the app through its host, schemas or queue definitions; list those public module paths in tests/support-imports.options.modules in the repository's quality config.`,
21
+ subject: edge.specifier,
22
+ }));
23
+ },
24
+ description: "Test-support reaches repository modules only through the public surface named by the repository's module allowlist.",
25
+ id: "tests/support-imports",
26
+ options: Schema.toStandardSchemaV1(SupportImportsOptions, { parseOptions: { errors: "all", onExcessProperty: "error" } }),
27
+ });
28
+ //# sourceMappingURL=supportImports.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"supportImports.js","sourceRoot":"","sources":["../../../src/rules/stories/supportImports.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7C,MAAM,qBAAqB,GAAG,MAAM,CAAC,MAAM,CAAC;IAC3C,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,sBAAsB,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC;CAC5F,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,cAAc,GAAG,UAAU,CAAC;IACxC,KAAK,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,GAAG,MAAM,EAAE,EAAE,EAAE;QACvC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;YAC9D,OAAO,EAAE,CAAC;QACX,CAAC;QACD,MAAM,KAAK,GAAG,MAAM,WAAW,CAAC,MAAM,CAAC,CAAC;QACxC,OAAO,KAAK,CAAC,KAAK;aAChB,MAAM,CACN,CAAC,IAAI,EAAE,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,EAAE,CAAC,IAAI,KAAK,MAAM,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,CAC3I;aACA,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;YACf,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,OAAO,EAAE,yBAAyB,IAAI,CAAC,SAAS,iNAAiN;YACjQ,OAAO,EAAE,IAAI,CAAC,SAAS;SACvB,CAAC,CAAC,CAAC;IACN,CAAC;IACD,WAAW,EAAE,qHAAqH;IAClI,EAAE,EAAE,uBAAuB;IAC3B,OAAO,EAAE,MAAM,CAAC,kBAAkB,CAAC,qBAAqB,EAAE,EAAE,YAAY,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAE,EAAE,CAAC;CACzH,CAAC,CAAC"}
@@ -0,0 +1,11 @@
1
+ import ts from "typescript";
2
+ import type { Finding, SourceFile } from "#rule.ts";
3
+ export declare function isSpec(path: string): boolean;
4
+ export declare function unwrapped(node: ts.Expression): ts.Expression;
5
+ export declare function memberName(node: ts.Expression): string | undefined;
6
+ export declare function numericValue(node: ts.Expression): number | undefined;
7
+ export declare function findingAt(source: ts.SourceFile, node: ts.Node, message: string): Finding;
8
+ export declare function nodesWhere(source: ts.SourceFile, accepts: (node: ts.Node) => boolean): readonly ts.Node[];
9
+ export declare function topLevelHelpers(source: ts.SourceFile): readonly ts.Node[];
10
+ export declare function syntaxFindings(file: SourceFile, select: (source: ts.SourceFile) => readonly ts.Node[], message: string): readonly Finding[];
11
+ //# sourceMappingURL=syntax.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"syntax.d.ts","sourceRoot":"","sources":["../../../src/rules/stories/syntax.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,YAAY,CAAC;AAE5B,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAGpD,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE5C;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,EAAE,CAAC,UAAU,GAAG,EAAE,CAAC,UAAU,CAI5D;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,EAAE,CAAC,UAAU,GAAG,MAAM,GAAG,SAAS,CAMlE;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,EAAE,CAAC,UAAU,GAAG,MAAM,GAAG,SAAS,CAUpE;AAED,wBAAgB,SAAS,CAAC,MAAM,EAAE,EAAE,CAAC,UAAU,EAAE,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAExF;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,EAAE,CAAC,UAAU,EAAE,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,OAAO,GAAG,SAAS,EAAE,CAAC,IAAI,EAAE,CAUzG;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,EAAE,CAAC,UAAU,GAAG,SAAS,EAAE,CAAC,IAAI,EAAE,CAYzE;AAaD,wBAAgB,cAAc,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,MAAM,GAAG,SAAS,OAAO,EAAE,CAG3I"}
@@ -0,0 +1,71 @@
1
+ import ts from "typescript";
2
+ import { isTestSupport, testName } from "#naming/testName.ts";
3
+ import { parse } from "#rules/syntax.ts";
4
+ export function isSpec(path) {
5
+ return testName(path)?.kind === "spec" && !isTestSupport(path);
6
+ }
7
+ export function unwrapped(node) {
8
+ return ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isSatisfiesExpression(node) || ts.isNonNullExpression(node)
9
+ ? unwrapped(node.expression)
10
+ : node;
11
+ }
12
+ export function memberName(node) {
13
+ const value = unwrapped(node);
14
+ if (ts.isPropertyAccessExpression(value)) {
15
+ return value.name.text;
16
+ }
17
+ return ts.isElementAccessExpression(value) && ts.isStringLiteralLike(value.argumentExpression) ? value.argumentExpression.text : undefined;
18
+ }
19
+ export function numericValue(node) {
20
+ const value = unwrapped(node);
21
+ if (ts.isNumericLiteral(value)) {
22
+ return Number(value.text);
23
+ }
24
+ if (ts.isPrefixUnaryExpression(value) && (value.operator === ts.SyntaxKind.MinusToken || value.operator === ts.SyntaxKind.PlusToken)) {
25
+ const operand = numericValue(value.operand);
26
+ return operand === undefined ? undefined : operand * (value.operator === ts.SyntaxKind.MinusToken ? -1 : 1);
27
+ }
28
+ return undefined;
29
+ }
30
+ export function findingAt(source, node, message) {
31
+ return { file: source.fileName, line: source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1, message };
32
+ }
33
+ export function nodesWhere(source, accepts) {
34
+ const found = [];
35
+ function visit(node) {
36
+ if (accepts(node)) {
37
+ found.push(node);
38
+ }
39
+ ts.forEachChild(node, visit);
40
+ }
41
+ visit(source);
42
+ return found;
43
+ }
44
+ export function topLevelHelpers(source) {
45
+ return source.statements.flatMap((statement) => {
46
+ if (ts.isFunctionDeclaration(statement)) {
47
+ return statement.body === undefined ? [] : [statement.name ?? statement];
48
+ }
49
+ return ts.isVariableStatement(statement)
50
+ ? statement.declarationList.declarations.flatMap((declaration) => {
51
+ const value = declaration.initializer === undefined ? undefined : unwrapped(declaration.initializer);
52
+ return value !== undefined && functionValue(value) ? [declaration.name] : [];
53
+ })
54
+ : [];
55
+ });
56
+ }
57
+ function functionValue(value) {
58
+ const node = unwrapped(value);
59
+ if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
60
+ return true;
61
+ }
62
+ if (!ts.isCallExpression(node)) {
63
+ return false;
64
+ }
65
+ return ["fn", "fnUntraced"].includes(memberName(node.expression) ?? "") || (ts.isCallExpression(node.expression) && functionValue(node.expression));
66
+ }
67
+ export function syntaxFindings(file, select, message) {
68
+ const source = parse(file);
69
+ return source === undefined ? [] : select(source).map((node) => findingAt(source, node, message));
70
+ }
71
+ //# sourceMappingURL=syntax.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"syntax.js","sourceRoot":"","sources":["../../../src/rules/stories/syntax.ts"],"names":[],"mappings":"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"}
@@ -76,6 +76,8 @@ help: fix what the output above reports, then commit again; `quality hooks pre-c
76
76
 
77
77
  The hook is a short script in the repository's common git directory, `.git/hooks/pre-commit`, which every worktree of the repository shares. It changes to the root of the worktree that commits, then to the config's folder in it, and runs that worktree's quality with that worktree's config, so a branch that changes the checks commits under its own rules. It runs quality the way `hooks install` ran: when that was a file inside the repository, such as a workspace's own source, it runs `node` with the same Node options on that file; otherwise it runs `node_modules/.bin/quality` in the config's folder. When that file does not exist, as in a new worktree whose dependencies are not installed yet, the hook blocks the commit and says to install them. A config file not named `quality.config.ts` is passed with `--config`.
78
78
 
79
- `hooks install` never sets `core.hooksPath`, so a hooks path set for the whole machine, such as a check that calls each repository's own hook after its own, keeps working. When `core.hooksPath` is set, git runs the hook found there instead, and `hooks install` notes that the installed hook runs only when that one calls it.
79
+ `hooks install` sets repository-local `core.hooksPath` to the common git directory's `hooks` folder and prints that path. It never changes global or system git configuration. At commit time, the hook reads global `core.hooksPath` and runs its executable `pre-commit` hook from the committing worktree's root, before Quality's checks. Relative global paths resolve from that root; a global path pointing to Quality's own hook is not chained. Quality still runs when the global hook fails, and either failure blocks the commit. Installation reports whether there is an executable global hook to chain.
80
80
 
81
- A pre-commit hook that quality did not write is kept: `hooks install` warns, says how to call quality from it, and exits 0. `--force` replaces it. `quality hooks uninstall` removes quality's hook and keeps any other.
81
+ `quality hooks status` reports whether git's effective hooks path runs an executable Quality pre-commit hook, and exits 1 with an installation hint when it does not. It also reports global chaining when Quality is active.
82
+
83
+ A pre-commit hook that quality did not write is kept: `hooks install` warns, says how to call quality from it, and exits 0 without setting the hooks path. `--force` replaces it. `quality hooks uninstall` removes quality's hook and keeps any other. It removes the repository-local hooks path only when that setting still points to Quality's hooks directory, letting global hooks take effect again.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shivaedev/quality",
3
- "version": "0.27.0",
3
+ "version": "0.28.0",
4
4
  "description": "Repository quality gate with typed rules, a baseline of existing debt and a registry of reasoned exceptions",
5
5
  "homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/quality#readme",
6
6
  "bugs": {
@@ -34,7 +34,10 @@
34
34
  "./package.json": "./package.json",
35
35
  "./domEnvironment.config.ts": null,
36
36
  "./imports/isTooling.ts": null,
37
- "./rules/imports/bundleInputs.ts": null
37
+ "./rules/imports/bundleInputs.ts": null,
38
+ "./inventory/shadcnFolders.ts": null,
39
+ "./inventory/shadcnAlias.ts": null,
40
+ "./naming/migrationFile.ts": null
38
41
  },
39
42
  "bin": {
40
43
  "quality": "./dist/cli.js"
package/src/biome/run.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { createRequire } from "node:module";
3
3
  import process from "node:process";
4
+ import { NodeFileSystem } from "@effect/platform-node";
5
+ import { Effect } from "effect";
4
6
  import type { ExceptionDeclaration } from "#exceptions/declaration.ts";
7
+ import { shadcnFolders } from "#inventory/shadcnFolders.ts";
5
8
  import { withExceptionConfig } from "./exceptionConfig.ts";
6
9
 
7
10
  export interface BiomeRun {
@@ -12,10 +15,21 @@ export interface BiomeRun {
12
15
 
13
16
  const BIN = createRequire(import.meta.url).resolve("@biomejs/biome/bin/biome");
14
17
 
15
- export function runBiome(root: string, args: readonly string[], exceptions: readonly ExceptionDeclaration[] = []): Promise<BiomeRun> {
18
+ export async function runBiome(root: string, args: readonly string[], exceptions: readonly ExceptionDeclaration[] = []): Promise<BiomeRun> {
19
+ const folders = await Effect.runPromise(shadcnFolders(root).pipe(Effect.provide(NodeFileSystem.layer)));
20
+ const toolOwned: readonly ExceptionDeclaration[] =
21
+ folders.length === 0
22
+ ? []
23
+ : [
24
+ {
25
+ files: ["**", ...folders.map((folder) => `!${folder}`)],
26
+ reason: "components.json assigns these folders to the shadcn CLI.",
27
+ rules: ["biome/files/includes"],
28
+ },
29
+ ];
16
30
  return withExceptionConfig(
17
31
  root,
18
- exceptions,
32
+ [...exceptions, ...toolOwned],
19
33
  (configArgs) =>
20
34
  new Promise((resolve, reject) => {
21
35
  execFile(process.execPath, [BIN, ...args, ...configArgs, "--colors=off"], { cwd: root, maxBuffer: 2 ** 30 }, (error, stdout, stderr) => {
package/src/cli/args.ts CHANGED
@@ -12,6 +12,7 @@ export type Command =
12
12
  | { readonly _tag: "BaselineMigrate"; readonly config: string | undefined; readonly from: string | undefined }
13
13
  | { readonly _tag: "HooksInstall"; readonly config: string | undefined; readonly force: boolean }
14
14
  | { readonly _tag: "HooksUninstall" }
15
+ | { readonly _tag: "HooksStatus" }
15
16
  | { readonly _tag: "HooksPreCommit"; readonly config: string | undefined }
16
17
  | { readonly _tag: "Help" };
17
18
 
@@ -52,6 +53,7 @@ const ACCEPTS: Readonly<Record<string, readonly Option[]>> = {
52
53
  fix: [],
53
54
  "hooks install": ["force"],
54
55
  "hooks pre-commit": [],
56
+ "hooks status": [],
55
57
  "hooks uninstall": [],
56
58
  lint: [],
57
59
  };
@@ -85,6 +87,8 @@ function commandFor(name: string, values: Values): Parsed {
85
87
  return parsed({ _tag: "HooksInstall", config, force: values.force === true });
86
88
  case "hooks uninstall":
87
89
  return parsed({ _tag: "HooksUninstall" });
90
+ case "hooks status":
91
+ return parsed({ _tag: "HooksStatus" });
88
92
  case "hooks pre-commit":
89
93
  return parsed({ _tag: "HooksPreCommit", config });
90
94
  default:
@@ -8,6 +8,7 @@ export const USAGE = `Usage:
8
8
  quality baseline migrate [--config <file>] [--from <file>]
9
9
  quality hooks install [--config <file>] [--force]
10
10
  quality hooks uninstall
11
+ quality hooks status
11
12
  quality hooks pre-commit [--config <file>]
12
13
 
13
14
  lint Run every rule. Exits 1 on an error-level violation, a file over its baseline, a stale registry entry
@@ -21,6 +22,7 @@ baseline migrate Move a baseline from the earlier JSON format (--from, quality/
21
22
  hooks install Install the git pre-commit hook, shared by every worktree, that runs hooks pre-commit. Keeps a hook that is not quality's
22
23
  unless --force replaces it.
23
24
  hooks uninstall Remove quality's pre-commit hook. Keeps a hook that is not quality's.
25
+ hooks status Check that git's effective hooks path runs Quality's pre-commit hook and report global chaining.
24
26
  hooks pre-commit What the hook runs: lint, then package.json scripts typecheck and quality:commit when present.
25
27
 
26
28
  --config <file> Config file; its directory is the repository root. Without this option, uses ./quality.config.ts when present, otherwise defaults.
@@ -30,5 +32,5 @@ Exit codes: 0 passed, 1 failed the gate, 2 could not run.`;
30
32
  export const SUBCOMMANDS: Readonly<Record<string, string>> = {
31
33
  baseline: "baseline takes write, prune, tighten or migrate.",
32
34
  exceptions: "exceptions takes migrate.",
33
- hooks: "hooks takes install, uninstall or pre-commit.",
35
+ hooks: "hooks takes install, uninstall, status or pre-commit.",
34
36
  };
@@ -10,6 +10,7 @@ import { SetupFailure } from "#failure.ts";
10
10
  import type { Launch } from "#hooks/command.ts";
11
11
  import { installHook } from "#hooks/install.ts";
12
12
  import { preCommitHook } from "#hooks/preCommit.ts";
13
+ import { hookStatus } from "#hooks/status.ts";
13
14
  import { uninstallHook } from "#hooks/uninstall.ts";
14
15
 
15
16
  export interface Invocation {
@@ -44,6 +45,8 @@ export const program = Effect.fn("Cli.program")(function* (args: readonly string
44
45
  return yield* installHook({ config: command.config, cwd, force: command.force, launch });
45
46
  case "HooksUninstall":
46
47
  return yield* uninstallHook(cwd);
48
+ case "HooksStatus":
49
+ return yield* hookStatus(cwd);
47
50
  case "HooksPreCommit":
48
51
  return yield* preCommitHook(cwd, command.config);
49
52
  }
@@ -1,12 +1,14 @@
1
- import { basename, dirname, join, relative, resolve } from "node:path";
1
+ import { basename, dirname, join, relative } from "node:path";
2
2
  import { Console, Effect, FileSystem } from "effect";
3
3
  import { CONFIG_FILE } from "#config/file.ts";
4
4
  import { loadConfig } from "#config/load.ts";
5
5
  import { SetupFailure } from "#failure.ts";
6
+ import { gitOrFail } from "#git/command.ts";
6
7
  import { readOptionalText } from "#inventory/filesystem.ts";
7
8
  import { commandWords, hookCommand, type Launch, shellWords } from "./command.ts";
8
- import { type HookLocation, hookLocation } from "./location.ts";
9
+ import { hookLocation } from "./location.ts";
9
10
  import { hookShim, isHookShim } from "./shim.ts";
11
+ import { globalHookStatus } from "./status.ts";
10
12
 
11
13
  export interface HookInstall {
12
14
  readonly config: string | undefined;
@@ -33,14 +35,6 @@ const write = Effect.fnUntraced(function* (file: string, text: string) {
33
35
  );
34
36
  });
35
37
 
36
- function elsewhere(location: HookLocation): Effect.Effect<void> {
37
- return location.hooksPath === undefined || resolve(location.toplevel, location.hooksPath) === dirname(location.file)
38
- ? Effect.void
39
- : Console.error(
40
- `note: core.hooksPath is ${location.hooksPath}, so git runs the pre-commit hook there; ${location.file} runs only when that hook calls it.`,
41
- );
42
- }
43
-
44
38
  export const installHook = Effect.fn("Hooks.installHook")(function* (install: HookInstall) {
45
39
  const config = yield* loadConfig(install.cwd, install.config);
46
40
  const location = yield* hookLocation(config.root);
@@ -58,12 +52,17 @@ export const installHook = Effect.fn("Hooks.installHook")(function* (install: Ho
58
52
  yield* Console.error(
59
53
  `quality: kept ${location.file}: it is not quality's pre-commit hook, so quality does not check commits.\nhelp: call \`${call}\` from that hook, or replace it with \`quality hooks install --force\`.`,
60
54
  );
61
- } else if (existing === shim) {
55
+ return;
56
+ }
57
+ if (existing === shim) {
62
58
  yield* Console.log(`quality: the pre-commit hook at ${location.file} is up to date.`);
63
59
  } else {
64
60
  yield* write(location.file, shim);
65
61
  const done = foreign ? `replaced ${location.file}, which was not quality's hook` : `installed the pre-commit hook at ${location.file}`;
66
62
  yield* Console.log(`quality: ${done}; each commit runs \`${words}\`.`);
67
63
  }
68
- yield* elsewhere(location);
64
+ const hooks = dirname(location.file);
65
+ yield* gitOrFail(config.root, ["config", "--local", "core.hooksPath", hooks]);
66
+ yield* Console.log(`quality: set repository-local core.hooksPath to ${hooks}.`);
67
+ yield* globalHookStatus(location);
69
68
  });