@starci/eslint-canon-be 3.0.10 → 3.2.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 (149) hide show
  1. package/CHANGELOG.md +7 -1
  2. package/README.md +2 -1
  3. package/builders.mjs +1 -1
  4. package/comments.mjs +1 -1
  5. package/config-owner.mjs +1 -1
  6. package/index.mjs +2 -0
  7. package/lib/config.mjs +4 -1
  8. package/lib/doors.mjs +3 -1
  9. package/module-shape.mjs +1 -2
  10. package/package.json +1 -1
  11. package/resilience.mjs +1 -1
  12. package/runtime/config.example.yaml +194 -0
  13. package/runtime/engine/by-code-unit.mjs +12 -0
  14. package/runtime/engine/config.mjs +222 -239
  15. package/runtime/engine/invalid-config.mjs +5 -5
  16. package/runtime/engine/model-config.mjs +90 -0
  17. package/runtime/engine/orca-config.mjs +3 -1
  18. package/runtime/engine/release-config.mjs +28 -0
  19. package/runtime/engine/removed-vocabulary.mjs +28 -0
  20. package/runtime/engine/resources-config.mjs +52 -0
  21. package/runtime/engine/runtime-root.mjs +17 -0
  22. package/runtime/engine/secrets.mjs +35 -31
  23. package/runtime/engine/sonar-config.mjs +21 -0
  24. package/runtime/engine/temp-root.mjs +32 -0
  25. package/runtime/knowledge/hfs/canon-pins.yaml +14 -14
  26. package/runtime/knowledge/hfs/rules.yaml +161 -65
  27. package/runtime/knowledge/hfs/slots.yaml +11 -14
  28. package/runtime/modules/kernel/removed-vocabulary.yaml +268 -0
  29. package/runtime/modules/models/registry.yaml +25 -359
  30. package/runtime/modules/models/runtimes.yaml +126 -265
  31. package/runtime/modules/models/tiers.yaml +66 -0
  32. package/runtime/scripts/api/git/lib.mjs +34 -1
  33. package/runtime/scripts/hfs/allows.mjs +3 -3
  34. package/runtime/scripts/hfs/architecture/ast-walks.mjs +63 -50
  35. package/runtime/scripts/hfs/architecture/automatic-gates.mjs +96 -0
  36. package/runtime/scripts/hfs/architecture/backend.mjs +261 -175
  37. package/runtime/scripts/hfs/architecture/background-unowned.mjs +69 -34
  38. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +55 -54
  39. package/runtime/scripts/hfs/architecture/clones.mjs +179 -106
  40. package/runtime/scripts/hfs/architecture/config-unread.mjs +5 -22
  41. package/runtime/scripts/hfs/architecture/config.mjs +95 -80
  42. package/runtime/scripts/hfs/architecture/connection-map.mjs +233 -141
  43. package/runtime/scripts/hfs/architecture/constructor-deps.mjs +15 -9
  44. package/runtime/scripts/hfs/architecture/context-coupling.mjs +76 -52
  45. package/runtime/scripts/hfs/architecture/context-map.mjs +215 -141
  46. package/runtime/scripts/hfs/architecture/context-owner.mjs +39 -30
  47. package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +31 -17
  48. package/runtime/scripts/hfs/architecture/context-transaction.mjs +38 -25
  49. package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +86 -40
  50. package/runtime/scripts/hfs/architecture/contracts-readonly.mjs +342 -0
  51. package/runtime/scripts/hfs/architecture/contracts.mjs +263 -455
  52. package/runtime/scripts/hfs/architecture/cross-app-duplicate.mjs +43 -34
  53. package/runtime/scripts/hfs/architecture/dead-exports.mjs +201 -154
  54. package/runtime/scripts/hfs/architecture/default-deny.mjs +118 -93
  55. package/runtime/scripts/hfs/architecture/doc-language.mjs +2 -1
  56. package/runtime/scripts/hfs/architecture/entrypoint.mjs +45 -30
  57. package/runtime/scripts/hfs/architecture/error-codes.mjs +19 -12
  58. package/runtime/scripts/hfs/architecture/error-masked.mjs +39 -26
  59. package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +2 -1
  60. package/runtime/scripts/hfs/architecture/feature-shape.mjs +26 -20
  61. package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
  62. package/runtime/scripts/hfs/architecture/frontend-grammar.mjs +190 -0
  63. package/runtime/scripts/hfs/architecture/frontend-routing.mjs +186 -0
  64. package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +311 -0
  65. package/runtime/scripts/hfs/architecture/frontend-world.mjs +328 -0
  66. package/runtime/scripts/hfs/architecture/frontend.mjs +109 -829
  67. package/runtime/scripts/hfs/architecture/hfs-graph.mjs +14 -2
  68. package/runtime/scripts/hfs/architecture/hfs.mjs +120 -288
  69. package/runtime/scripts/hfs/architecture/hooks-are-hooks.mjs +46 -26
  70. package/runtime/scripts/hfs/architecture/i18n-keys.mjs +114 -76
  71. package/runtime/scripts/hfs/architecture/index.mjs +164 -104
  72. package/runtime/scripts/hfs/architecture/injection-token-exported.mjs +33 -28
  73. package/runtime/scripts/hfs/architecture/machine-ast.mjs +180 -144
  74. package/runtime/scripts/hfs/architecture/managed-scripts.mjs +8 -2
  75. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +150 -101
  76. package/runtime/scripts/hfs/architecture/next-data-contract.mjs +51 -43
  77. package/runtime/scripts/hfs/architecture/next-data-key.mjs +338 -0
  78. package/runtime/scripts/hfs/architecture/next-data.mjs +179 -427
  79. package/runtime/scripts/hfs/architecture/owners.mjs +27 -13
  80. package/runtime/scripts/hfs/architecture/package-shape.mjs +51 -23
  81. package/runtime/scripts/hfs/architecture/presentation.mjs +165 -0
  82. package/runtime/scripts/hfs/architecture/reachability.mjs +127 -61
  83. package/runtime/scripts/hfs/architecture/register-once.mjs +112 -63
  84. package/runtime/scripts/hfs/architecture/registration.mjs +187 -110
  85. package/runtime/scripts/hfs/architecture/required-files.mjs +121 -70
  86. package/runtime/scripts/hfs/architecture/route-files-thin.mjs +83 -58
  87. package/runtime/scripts/hfs/architecture/schema-owner.mjs +182 -108
  88. package/runtime/scripts/hfs/architecture/source-names-shape.mjs +318 -0
  89. package/runtime/scripts/hfs/architecture/source-names.mjs +83 -248
  90. package/runtime/scripts/hfs/architecture/sql-owner.mjs +175 -118
  91. package/runtime/scripts/hfs/architecture/sql-returning.mjs +39 -28
  92. package/runtime/scripts/hfs/architecture/sql-tokens.mjs +302 -172
  93. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +1 -1
  94. package/runtime/scripts/hfs/architecture/supabase-be.mjs +55 -45
  95. package/runtime/scripts/hfs/architecture/supabase-results.mjs +182 -0
  96. package/runtime/scripts/hfs/architecture/supabase-tables.mjs +20 -12
  97. package/runtime/scripts/hfs/architecture/supabase.mjs +191 -263
  98. package/runtime/scripts/hfs/architecture/symbols.mjs +175 -99
  99. package/runtime/scripts/hfs/architecture/test-world-files.mjs +82 -41
  100. package/runtime/scripts/hfs/architecture/tiers.mjs +120 -69
  101. package/runtime/scripts/hfs/architecture/transport-owner.mjs +84 -57
  102. package/runtime/scripts/hfs/architecture/type-context.mjs +282 -0
  103. package/runtime/scripts/hfs/architecture/typescript.mjs +116 -276
  104. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +84 -62
  105. package/runtime/scripts/hfs/declaration-shape.mjs +77 -36
  106. package/runtime/scripts/hfs/declaration-slots.mjs +1 -1
  107. package/runtime/scripts/hfs/edition-slots.mjs +5 -3
  108. package/runtime/scripts/hfs/linear-text.mjs +30 -0
  109. package/runtime/scripts/hfs/manifest-shape.mjs +164 -63
  110. package/runtime/scripts/hfs/path-findings.mjs +58 -38
  111. package/runtime/scripts/hfs/project-graph.mjs +4 -1
  112. package/runtime/scripts/hfs/repo-identity.mjs +21 -4
  113. package/runtime/scripts/hfs/rule-catalog.mjs +194 -0
  114. package/runtime/scripts/hfs/rule-params-shape.mjs +33 -27
  115. package/runtime/scripts/hfs/rules/fe-no-tests.mjs +36 -16
  116. package/runtime/scripts/hfs/rules/secrets.mjs +4 -3
  117. package/runtime/scripts/hfs/slot-app-view.mjs +102 -0
  118. package/runtime/scripts/hfs/slot-classify.mjs +65 -0
  119. package/runtime/scripts/hfs/slot-errors.mjs +11 -0
  120. package/runtime/scripts/hfs/slot-imports.mjs +76 -0
  121. package/runtime/scripts/hfs/slot-manifest-shape.mjs +73 -0
  122. package/runtime/scripts/hfs/slot-match.mjs +153 -0
  123. package/runtime/scripts/hfs/slot-path.mjs +8 -0
  124. package/runtime/scripts/hfs/slot-required.mjs +76 -0
  125. package/runtime/scripts/hfs/slot-semantic-problems.mjs +148 -0
  126. package/runtime/scripts/hfs/slot-side-problems.mjs +28 -0
  127. package/runtime/scripts/hfs/slots.mjs +74 -695
  128. package/runtime/scripts/hfs/trailing-slashes.mjs +8 -0
  129. package/runtime/scripts/hfs/typescript-programs.mjs +6 -4
  130. package/runtime/scripts/hfs/view.mjs +1 -1
  131. package/runtime/scripts/lib/dockerfile.mjs +56 -33
  132. package/runtime/scripts/lib/git.mjs +2 -2
  133. package/runtime/scripts/lib/i18n.mjs +3 -2
  134. package/runtime/scripts/lib/language.mjs +3 -3
  135. package/runtime/scripts/lib/list.mjs +8 -1
  136. package/runtime/scripts/lib/path-key.mjs +38 -5
  137. package/runtime/scripts/lib/regex.mjs +1 -1
  138. package/runtime/scripts/lib/secret-patterns.mjs +6 -2
  139. package/runtime/scripts/lib/sonar-syntax-rules.mjs +225 -0
  140. package/runtime/scripts/lib/sops-envelope.mjs +95 -48
  141. package/runtime/scripts/lib/stack-services.mjs +1 -2
  142. package/runtime/scripts/lib/ts-ast.mjs +9 -0
  143. package/runtime/scripts/lib/walk.mjs +6 -1
  144. package/runtime/scripts/lib/yaml-cached.mjs +14 -0
  145. package/schema-authority.mjs +1 -1
  146. package/service-deps.mjs +1 -1
  147. package/sonar-parity.mjs +19 -0
  148. package/suppression.mjs +2 -6
  149. package/test-world.mjs +2 -10
@@ -25,7 +25,12 @@ export function testSecretPaths(name, { repo, stack = 'dev' } = {}) {
25
25
  export const setCommand = (name = '<name>', stack = 'dev') => `node scripts/stack-secret.mjs set ${stack}/secrets/test/${name}`;
26
26
 
27
27
  /** sops' format for a plaintext path, as the repository's stack-secret.mjs formatFor decides it. */
28
- export const sopsFormatFor = (file) => (file.endsWith('.env') ? 'dotenv' : /\.json$/.test(file) ? 'json' : /\.(ya?ml|kubeconfig)$/.test(file) ? 'yaml' : 'binary');
28
+ export const sopsFormatFor = (file) => {
29
+ if (file.endsWith('.env')) return 'dotenv';
30
+ if (file.endsWith('.json')) return 'json';
31
+ if (/\.(ya?ml|kubeconfig)$/.test(file)) return 'yaml';
32
+ return 'binary';
33
+ };
29
34
 
30
35
  /** Every leaf outside the `sops` block is an ENC[...] value (or null); the block itself carries a mac. */
31
36
  const allEncrypted = (node) => {
@@ -48,7 +53,7 @@ export function isSopsEnvelope(text) {
48
53
  if (!src.trim()) return false;
49
54
  try { return sopsTree(JSON.parse(src)); } catch { /* not JSON */ }
50
55
  const lines = src.split(/\r?\n/).filter((l) => l.trim() && !/^\s*#/.test(l));
51
- if (lines.length && lines.every((l) => /^[A-Za-z_][A-Za-z0-9_]*=/.test(l))) {
56
+ if (lines.length && lines.every((l) => /^[A-Za-z_]\w*=/.test(l))) {
52
57
  const pairs = lines.map((l) => [l.slice(0, l.indexOf('=')), l.slice(l.indexOf('=') + 1)]);
53
58
  const meta = pairs.filter(([k]) => k.startsWith('sops_'));
54
59
  const vals = pairs.filter(([k]) => !k.startsWith('sops_'));
@@ -58,55 +63,97 @@ export function isSopsEnvelope(text) {
58
63
  }
59
64
 
60
65
  /** Admit one flat age envelope for the native selected-identity invocation; this is shape admission, never cryptographic proof. */
61
- export function selectedAgeEnvelope(text, format, recipient) {
62
- const refuse = reason => ({ ok: false, reason });
63
- if (!['binary', 'json', 'yaml', 'dotenv'].includes(format)) return refuse('unsupported-format');
64
- let meta;
66
+ const refusal = (reason) => ({ ok: false, reason });
67
+
68
+ function dotenvMetadataValue(meta, age, key, value) {
69
+ if (key === 'age__list_0__map_recipient') age.recipient = value;
70
+ else if (key === 'age__list_0__map_enc') age.enc = value;
71
+ else if (key === 'mac_only_encrypted') {
72
+ if (value !== 'false') return 'partial-mac';
73
+ meta[key] = false;
74
+ } else if (key.includes('__')) return 'unsupported-key-group';
75
+ else meta[key] = value;
76
+ return null;
77
+ }
78
+
79
+ function readDotenvMetadata(text) {
80
+ const pairs = new Map();
81
+ for (const line of String(text).split('\n')) {
82
+ if (!line || line.startsWith('#')) continue;
83
+ if (line.includes('\r')) return { refusal: 'unsupported-dotenv' };
84
+ const at = line.indexOf('=');
85
+ if (at < 1) return { refusal: 'unsupported-dotenv' };
86
+ const name = line.slice(0, at);
87
+ if (pairs.has(name)) return { refusal: 'duplicate-key' };
88
+ pairs.set(name, line.slice(at + 1).replaceAll(String.raw`\n`, '\n'));
89
+ }
90
+ const meta = {};
91
+ const age = {};
92
+ for (const [name, value] of pairs) {
93
+ if (!name.startsWith('sops_')) continue;
94
+ const reason = dotenvMetadataValue(meta, age, name.slice('sops_'.length), value);
95
+ if (reason) return { refusal: reason };
96
+ }
97
+ meta.age = [age];
98
+ return { meta };
99
+ }
100
+
101
+ function readStructuredMetadata(text, format) {
102
+ const doc = parseYaml(String(text));
103
+ if (format !== 'yaml') JSON.parse(String(text));
104
+ if (format === 'binary' && (!doc || Object.keys(doc).length !== 2 || !Object.hasOwn(doc, 'data') || !Object.hasOwn(doc, 'sops') || typeof doc.data !== 'string'))
105
+ return { refusal: 'unsupported-binary-shape' };
106
+ return { meta: doc?.sops };
107
+ }
108
+
109
+ function readAgeMetadata(text, format) {
65
110
  try {
66
- if (format === 'dotenv') {
67
- const pairs = new Map();
68
- for (const line of String(text).split('\n')) {
69
- if (!line || line.startsWith('#')) continue;
70
- if (line.includes('\r')) return refuse('unsupported-dotenv');
71
- const at = line.indexOf('=');
72
- if (at < 1) return refuse('unsupported-dotenv');
73
- const name = line.slice(0, at);
74
- if (pairs.has(name)) return refuse('duplicate-key');
75
- pairs.set(name, line.slice(at + 1).replaceAll('\\n', '\n'));
76
- }
77
- meta = {};
78
- const age = {};
79
- for (const [name, value] of pairs) {
80
- if (!name.startsWith('sops_')) continue;
81
- const key = name.slice('sops_'.length);
82
- if (key === 'age__list_0__map_recipient') age.recipient = value;
83
- else if (key === 'age__list_0__map_enc') age.enc = value;
84
- else if (key === 'mac_only_encrypted') {
85
- if (value !== 'false') return refuse('partial-mac');
86
- meta[key] = false;
87
- } else if (key.includes('__')) return refuse('unsupported-key-group');
88
- else meta[key] = value;
89
- }
90
- meta.age = [age];
91
- } else {
92
- const doc = parseYaml(String(text));
93
- if (format !== 'yaml') JSON.parse(String(text));
94
- if (format === 'binary' && (!doc || Object.keys(doc).length !== 2 || !Object.hasOwn(doc, 'data') || !Object.hasOwn(doc, 'sops') || typeof doc.data !== 'string')) return refuse('unsupported-binary-shape');
95
- meta = doc?.sops;
96
- }
97
- } catch { return refuse('invalid-envelope'); }
98
- if (!meta || typeof meta !== 'object' || Array.isArray(meta) || !isSopsEnvelope(text)) return refuse('invalid-envelope');
99
- if (Object.hasOwn(meta, 'key_groups') || Object.hasOwn(meta, 'shamir_threshold')) return refuse('unsupported-key-group');
111
+ return format === 'dotenv' ? readDotenvMetadata(text) : readStructuredMetadata(text, format);
112
+ } catch { return { refusal: 'invalid-envelope' }; }
113
+ }
114
+
115
+ function hasNonAgeProvider(meta, providers) {
116
+ for (const name of providers) {
117
+ if (Object.hasOwn(meta, name) && (!Array.isArray(meta[name]) || meta[name].length)) return true;
118
+ }
119
+ return false;
120
+ }
121
+
122
+ function isAgeEntry(entry) {
123
+ return !!entry && typeof entry === 'object' && !Array.isArray(entry)
124
+ && !Object.keys(entry).some(name => !['recipient', 'enc'].includes(name));
125
+ }
126
+
127
+ function hasValidAgePayload(entry) {
128
+ return typeof entry.enc === 'string' && entry.enc.startsWith('-----BEGIN AGE ENCRYPTED FILE-----\n')
129
+ && entry.enc.includes('\n-----END AGE ENCRYPTED FILE-----');
130
+ }
131
+
132
+ function hasValidEnvelopeVersion(meta) {
133
+ return typeof meta.version === 'string' && /^\d+\.\d+\.\d+$/.test(meta.version)
134
+ && typeof meta.lastmodified === 'string' && Number.isFinite(Date.parse(meta.lastmodified));
135
+ }
136
+
137
+ function validateAgeMetadata(meta, text, recipient) {
138
+ if (!meta || typeof meta !== 'object' || Array.isArray(meta) || !isSopsEnvelope(text)) return refusal('invalid-envelope');
139
+ if (Object.hasOwn(meta, 'key_groups') || Object.hasOwn(meta, 'shamir_threshold')) return refusal('unsupported-key-group');
100
140
  const providers = ['kms', 'gcp_kms', 'hckms', 'azure_kv', 'hc_vault', 'pgp'];
101
- for (const name of providers) if (Object.hasOwn(meta, name) && (!Array.isArray(meta[name]) || meta[name].length)) return refuse('non-age-provider');
141
+ if (hasNonAgeProvider(meta, providers)) return refusal('non-age-provider');
102
142
  const fields = new Set([...providers, 'age', 'lastmodified', 'mac', 'version', 'unencrypted_suffix', 'encrypted_suffix', 'unencrypted_regex', 'encrypted_regex', 'unencrypted_comment_regex', 'encrypted_comment_regex', 'mac_only_encrypted']);
103
- if (Object.keys(meta).some(name => !fields.has(name))) return refuse('unknown-metadata');
104
- if (Object.hasOwn(meta, 'mac_only_encrypted') && meta.mac_only_encrypted !== false) return refuse('partial-mac');
105
- if (!Array.isArray(meta.age) || meta.age.length !== 1) return refuse('unsupported-age-set');
143
+ if (Object.keys(meta).some(name => !fields.has(name))) return refusal('unknown-metadata');
144
+ if (Object.hasOwn(meta, 'mac_only_encrypted') && meta.mac_only_encrypted !== false) return refusal('partial-mac');
145
+ if (!Array.isArray(meta.age) || meta.age.length !== 1) return refusal('unsupported-age-set');
106
146
  const entry = meta.age[0];
107
- if (!entry || typeof entry !== 'object' || Array.isArray(entry) || Object.keys(entry).some(name => !['recipient', 'enc'].includes(name))) return refuse('invalid-age-entry');
108
- if (typeof entry.recipient !== 'string' || entry.recipient !== recipient) return refuse('recipient-mismatch');
109
- if (typeof entry.enc !== 'string' || !entry.enc.startsWith('-----BEGIN AGE ENCRYPTED FILE-----\n') || !entry.enc.includes('\n-----END AGE ENCRYPTED FILE-----')) return refuse('invalid-age-entry');
110
- if (typeof meta.version !== 'string' || !/^\d+\.\d+\.\d+$/.test(meta.version) || typeof meta.lastmodified !== 'string' || !Number.isFinite(Date.parse(meta.lastmodified))) return refuse('invalid-envelope');
147
+ if (!isAgeEntry(entry)) return refusal('invalid-age-entry');
148
+ if (typeof entry.recipient !== 'string' || entry.recipient !== recipient) return refusal('recipient-mismatch');
149
+ if (!hasValidAgePayload(entry)) return refusal('invalid-age-entry');
150
+ if (!hasValidEnvelopeVersion(meta)) return refusal('invalid-envelope');
111
151
  return { ok: true };
112
152
  }
153
+
154
+ export function selectedAgeEnvelope(text, format, recipient) {
155
+ if (!['binary', 'json', 'yaml', 'dotenv'].includes(format)) return refusal('unsupported-format');
156
+ const parsed = readAgeMetadata(text, format);
157
+ if (parsed.refusal) return refusal(parsed.refusal);
158
+ return validateAgeMetadata(parsed.meta, text, recipient);
159
+ }
@@ -2,6 +2,7 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { parseYaml } from '../../engine/yaml.mjs';
4
4
  import { words } from './dockerfile.mjs';
5
+ import { asList } from './list.mjs';
5
6
 
6
7
  /**
7
8
  * The services of a repository's own stack definition, read from `.starcistacks/application-stacks.yaml` and the compose
@@ -74,8 +75,6 @@ const imagePathOf = image => {
74
75
  /** True when the image reference carries a tag or digest (an external image, not a placeholder of the repository's own build). */
75
76
  const isTagged = image => image.includes('@') || image.slice(image.lastIndexOf('/') + 1).includes(':');
76
77
 
77
- const asList = value => (Array.isArray(value) ? value : value === undefined || value === null ? [] : [value]);
78
-
79
78
  /** A compose `environment` (map or `KEY=VALUE` list) as a plain object; a `*_FILE` variable becomes the plain variable with the test value. */
80
79
  const environmentOf = value => {
81
80
  const entries = Array.isArray(value)
@@ -2,6 +2,15 @@
2
2
  // literal text, a node's 1-based position, a named property's initializer text. `ts` is the caller's
3
3
  // already-resolved compiler (see package-at.mjs loadTypescript); nothing here resolves a package.
4
4
 
5
+ /** Whether the single-bit `flag` (a ts.NodeFlags member) is set in `flags`: the bit read as arithmetic, no bitwise operator in the condition. */
6
+ export const hasFlag = (flags, flag) => Math.floor(flags / flag) % 2 === 1;
7
+
8
+ /** Whether any bit of `mask` (a ts flag or an OR of flags) is set in `flags` (absent flags read as none): an explicit comparison of the masked value. */
9
+ export const hasAnyFlag = (flags, mask) => ((flags ?? 0) & mask) !== 0;
10
+
11
+ /** Whether the variable `declaration` belongs to a `const` list: the TypeScript node-flag test, an explicit comparison of the masked flags. */
12
+ export const isConstVariable = (ts, declaration) => (ts.getCombinedNodeFlags(declaration.parent) & ts.NodeFlags.Const) !== 0;
13
+
5
14
  /** The text of an identifier or a string-literal-like `node`, or null (property names, literal specifiers). */
6
15
  export const nameText = (ts, node) => (node && (ts.isIdentifier(node) || ts.isStringLiteralLike(node)) ? node.text : null);
7
16
 
@@ -20,7 +20,12 @@ export function walkFiles(dir, {filter = () => true, sorted = false, exclude = (
20
20
  if (depth > maxDepth) return;
21
21
  let entries;
22
22
  try { entries = fs.readdirSync(current, {withFileTypes: true}); }
23
- catch (error) { if (ignoreReadErrors) return; throw error; }
23
+ catch (error) {
24
+ if (ignoreReadErrors) {
25
+ return;
26
+ }
27
+ throw error;
28
+ }
24
29
  if (sorted) entries = entries.sort((a, b) => a.name.localeCompare(b.name));
25
30
  for (const entry of entries) {
26
31
  const full = path.join(current, entry.name);
@@ -0,0 +1,14 @@
1
+ // yaml-cached.mjs - parse a YAML text once per distinct text in a process.
2
+ //
3
+ // The HFS slot manifest and rule catalog are large documents that one process asks for again at every check, every render and every fixture. Their
4
+ // bytes do not change between those asks, so the parse (the dominant cost of a load) is keyed by the text itself; each caller gets its own copy of the
5
+ // parsed document, so nothing a caller does to it reaches the next one. A changed file is a different text and parses again.
6
+ import { parseYaml } from '../../engine/yaml.mjs';
7
+
8
+ const parsed = new Map();
9
+
10
+ /** The parsed document of `text`: parsed on the first ask for this text, a structured copy afterwards. Throws what parseYaml throws, every time. */
11
+ export function parseYamlCached(text) {
12
+ if (!parsed.has(text)) parsed.set(text, parseYaml(text));
13
+ return structuredClone(parsed.get(text));
14
+ }
@@ -224,7 +224,7 @@ export const noRuntimeSchema = {
224
224
  const isEntityDecorator = (checker, decorator) => {
225
225
  const callee = ts.isCallExpression(decorator.expression) ? decorator.expression.expression : decorator.expression
226
226
  let symbol = checker.getSymbolAtLocation(callee)
227
- if (symbol && symbol.flags & ts.SymbolFlags.Alias) symbol = checker.getAliasedSymbol(symbol)
227
+ if (symbol && (symbol.flags & ts.SymbolFlags.Alias) !== 0) symbol = checker.getAliasedSymbol(symbol)
228
228
  return symbol?.name === "Entity" && (symbol.declarations ?? []).some((declaration) => packageOfFile(declaration.getSourceFile().fileName) === "typeorm")
229
229
  }
230
230
 
package/service-deps.mjs CHANGED
@@ -59,7 +59,7 @@ const isClassToken = (context, annotation) => {
59
59
  if (!tsNode) return false
60
60
  const type = checker.getTypeFromTypeNode(tsNode)
61
61
  const symbol = type.getSymbol()
62
- if (!symbol || !(symbol.flags & ts.SymbolFlags.Class)) return false
62
+ if (!symbol || (symbol.flags & ts.SymbolFlags.Class) === 0) return false
63
63
  const declaration = symbol.declarations?.find((entry) => ts.isClassDeclaration(entry) || ts.isClassExpression(entry))
64
64
  return Boolean(declaration)
65
65
  }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The law that keeps the code clear of the smells SonarCloud measures (R235 `SCAN_SMELL`).
3
+ *
4
+ * Each rule is the syntax-tree form of one Sonar rule, shared with the front-end canon (scripts/lib/sonar-syntax-rules.mjs,
5
+ * bundled under `runtime/`): `no-await-in-loop` (S9382), `prefer-code-point` (S7758), `prefer-string-raw` (S7780),
6
+ * `no-nested-conditional` (S3358), `no-void-operator` (S3735), `no-unused-import` (S1128) and `prefer-export-from` (S7763).
7
+ * The three Sonar rules that need the type checker, S1874 (a deprecated API), S6551 (an object turned into text) and S7503 (an `async`
8
+ * function that never awaits and returns no promise), are the typescript-eslint rules `no-deprecated`, `no-base-to-string` and
9
+ * `require-await` the factory turns on.
10
+ */
11
+ import { SONAR_SYNTAX_RULES } from "./runtime/scripts/lib/sonar-syntax-rules.mjs"
12
+
13
+ const NAMES = ["no-await-in-loop", "prefer-code-point", "prefer-string-raw", "no-nested-conditional", "no-void-operator", "no-unused-import", "prefer-export-from"]
14
+
15
+ /** The rules this law contributes to the plugin. */
16
+ export const rules = Object.fromEntries(NAMES.map((name) => [name, SONAR_SYNTAX_RULES[name]]))
17
+
18
+ /** Starts at error: a repository's fix lane clears the debt before the rule ships. */
19
+ export const recommended = Object.fromEntries(NAMES.map((name) => [`starci-be/${name}`, "error"]))
package/suppression.mjs CHANGED
@@ -9,8 +9,7 @@
9
9
  * - TypeScript: `@ts-ignore`, `@ts-expect-error` and `@ts-nocheck`, wherever they sit in a comment;
10
10
  * - coverage: `istanbul ignore`, `c8 ignore`, `v8 ignore`;
11
11
  * - Sonar: `NOSONAR` and `sonar-disable`;
12
- * - Prettier: `prettier-ignore`;
13
- * - the `vn-ok` language marker.
12
+ * - Prettier: `prettier-ignore`.
14
13
  *
15
14
  * The factory also sets `noInlineConfig: true`, so ESLint itself ignores such a comment; this rule is what turns the
16
15
  * ignored comment into a failure a person reads, instead of a silent no-op that looks like an exemption. There is no
@@ -24,7 +23,6 @@ const TS_DIRECTIVE = /@ts-(?:ignore|expect-error|nocheck)\b/
24
23
  const COVERAGE_IGNORE = /(?:^|[\s*/])(?:istanbul|c8|v8)\s+ignore\b/
25
24
  const SONAR = /\bNOSONAR\b|\bsonar-(?:disable|enable)\b/i
26
25
  const PRETTIER_IGNORE = /(?:^|[\s*/])prettier-ignore\b/
27
- const LANGUAGE_MARKER = /\bvn-ok\b/
28
26
 
29
27
  /** The finding a comment is, or null when it is ordinary prose. `/* global x *\/` is a block-comment form only. */
30
28
  const kindOf = (comment) => {
@@ -35,7 +33,6 @@ const kindOf = (comment) => {
35
33
  if (COVERAGE_IGNORE.test(text)) return "coverage"
36
34
  if (SONAR.test(text)) return "sonar"
37
35
  if (PRETTIER_IGNORE.test(text)) return "prettier"
38
- if (LANGUAGE_MARKER.test(text)) return "vn-ok"
39
36
  return null
40
37
  }
41
38
 
@@ -43,7 +40,7 @@ const kindOf = (comment) => {
43
40
  export const noInlineSuppression = {
44
41
  meta: {
45
42
  type: "problem",
46
- docs: { description: "No eslint, TypeScript, coverage, Sonar or Prettier suppression comment, and no vn-ok marker." },
43
+ docs: { description: "No eslint, TypeScript, coverage, Sonar or Prettier suppression comment." },
47
44
  schema: [],
48
45
  messages: {
49
46
  eslint: "An `eslint-disable`/`eslint-enable`, `eslint-env`, `/* global */` or rule-configuration comment. HFS does not switch a rule off or declare a global in place: fix the code, or propose a change to the rule in the canon.",
@@ -51,7 +48,6 @@ export const noInlineSuppression = {
51
48
  coverage: "An `istanbul`/`c8`/`v8 ignore` comment hides code from coverage. Write the test that covers it, or delete the code that nothing reaches.",
52
49
  sonar: "A `NOSONAR` or `sonar-disable` comment hides a finding from Sonar. Fix the finding.",
53
50
  prettier: "A `prettier-ignore` comment exempts code from the formatter. Let the formatter write it.",
54
- "vn-ok": "A `vn-ok` marker exempts a line from the language rule. HFS keeps no exemption marker: write the text in English, or move the data to a locale or data file.",
55
51
  },
56
52
  },
57
53
  create(context) {
package/test-world.mjs CHANGED
@@ -145,8 +145,7 @@ const plainKey = (property) => {
145
145
 
146
146
  /**
147
147
  * The world an integration spec (`{ modules }`, with real peer apps beside them as `apps` when its client calls another app of
148
- * ours) or an e2e spec (`{ apps }`, never `modules`) asks for; every such spec asks for one; the retired name `useE2eWorld`
149
- * exists nowhere.
148
+ * ours) or an e2e spec (`{ apps }`, never `modules`) asks for; every such spec asks for one.
150
149
  */
151
150
  export const testWorldShape = {
152
151
  meta: {
@@ -158,21 +157,14 @@ export const testWorldShape = {
158
157
  apps: "An e2e spec boots applications: call `useTestWorld({ apps: {...} })`, not `modules`.",
159
158
  literal: "The options of `useTestWorld(...)` must be one object literal with plain keys: a variable, a spread or a computed key hides whether the spec asks for `modules` or `apps`.",
160
159
  missing: "This spec never calls `useTestWorld(...)`: integration and e2e specs run inside the one test world.",
161
- retired: "`useE2eWorld` is the retired name of `useTestWorld`; there is one name, no alias.",
162
160
  },
163
161
  },
164
162
  create(context) {
165
163
  const slot = hfsOf(context).slotOf(context.filename || context.getFilename())
166
164
  const wants = slot === "be.tests.integration" ? "modules" : slot === "be.tests.e2e" ? "apps" : null
167
- const retired = {
168
- Identifier(node) {
169
- if (node.name === "useE2eWorld") context.report({ node, messageId: "retired" })
170
- },
171
- }
172
- if (!wants) return retired
165
+ if (!wants) return {}
173
166
  let seen = false
174
167
  return {
175
- ...retired,
176
168
  CallExpression(node) {
177
169
  if (node.callee.type !== "Identifier" || node.callee.name !== "useTestWorld") return
178
170
  seen = true