redlinegate 0.0.1

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 (198) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +188 -0
  3. package/commands/redline-init.md +54 -0
  4. package/commands/redline-verify.md +69 -0
  5. package/dist/bin/redline.js +605 -0
  6. package/dist/bin/redline.js.map +1 -0
  7. package/dist/commands/exempt.js +45 -0
  8. package/dist/commands/exempt.js.map +1 -0
  9. package/dist/commands/init.js +615 -0
  10. package/dist/commands/init.js.map +1 -0
  11. package/dist/commands/policy.js +25 -0
  12. package/dist/commands/policy.js.map +1 -0
  13. package/dist/commands/remove.js +427 -0
  14. package/dist/commands/remove.js.map +1 -0
  15. package/dist/commands/review.js +72 -0
  16. package/dist/commands/review.js.map +1 -0
  17. package/dist/commands/sync.js +42 -0
  18. package/dist/commands/sync.js.map +1 -0
  19. package/dist/commands/verify.js +478 -0
  20. package/dist/commands/verify.js.map +1 -0
  21. package/dist/config/redline-json.js +187 -0
  22. package/dist/config/redline-json.js.map +1 -0
  23. package/dist/core/errors.js +25 -0
  24. package/dist/core/errors.js.map +1 -0
  25. package/dist/core/git.js +152 -0
  26. package/dist/core/git.js.map +1 -0
  27. package/dist/core/log.js +22 -0
  28. package/dist/core/log.js.map +1 -0
  29. package/dist/core/severity.js +16 -0
  30. package/dist/core/severity.js.map +1 -0
  31. package/dist/core/version.js +15 -0
  32. package/dist/core/version.js.map +1 -0
  33. package/dist/detect/scan.js +79 -0
  34. package/dist/detect/scan.js.map +1 -0
  35. package/dist/detect/stack.js +71 -0
  36. package/dist/detect/stack.js.map +1 -0
  37. package/dist/enforce/ladder.js +148 -0
  38. package/dist/enforce/ladder.js.map +1 -0
  39. package/dist/exempt/parse.js +86 -0
  40. package/dist/exempt/parse.js.map +1 -0
  41. package/dist/metrics/options.js +257 -0
  42. package/dist/metrics/options.js.map +1 -0
  43. package/dist/metrics/run.js +72 -0
  44. package/dist/metrics/run.js.map +1 -0
  45. package/dist/platforms/azure/client.js +53 -0
  46. package/dist/platforms/azure/client.js.map +1 -0
  47. package/dist/platforms/azure/index.js +69 -0
  48. package/dist/platforms/azure/index.js.map +1 -0
  49. package/dist/platforms/azure/install.js +917 -0
  50. package/dist/platforms/azure/install.js.map +1 -0
  51. package/dist/platforms/azure/policy-types.js +70 -0
  52. package/dist/platforms/azure/policy-types.js.map +1 -0
  53. package/dist/platforms/azure/verify.js +309 -0
  54. package/dist/platforms/azure/verify.js.map +1 -0
  55. package/dist/platforms/detect.js +40 -0
  56. package/dist/platforms/detect.js.map +1 -0
  57. package/dist/platforms/github/client.js +49 -0
  58. package/dist/platforms/github/client.js.map +1 -0
  59. package/dist/platforms/github/index.js +54 -0
  60. package/dist/platforms/github/index.js.map +1 -0
  61. package/dist/platforms/github/install.js +558 -0
  62. package/dist/platforms/github/install.js.map +1 -0
  63. package/dist/platforms/github/push.js +102 -0
  64. package/dist/platforms/github/push.js.map +1 -0
  65. package/dist/platforms/github/remote.js +36 -0
  66. package/dist/platforms/github/remote.js.map +1 -0
  67. package/dist/platforms/github/verify.js +366 -0
  68. package/dist/platforms/github/verify.js.map +1 -0
  69. package/dist/platforms/http.js +58 -0
  70. package/dist/platforms/http.js.map +1 -0
  71. package/dist/platforms/pull-request-templates.js +157 -0
  72. package/dist/platforms/pull-request-templates.js.map +1 -0
  73. package/dist/platforms/remote.js +21 -0
  74. package/dist/platforms/remote.js.map +1 -0
  75. package/dist/platforms/resolve.js +69 -0
  76. package/dist/platforms/resolve.js.map +1 -0
  77. package/dist/platforms/shape.js +19 -0
  78. package/dist/platforms/shape.js.map +1 -0
  79. package/dist/platforms/types.js +24 -0
  80. package/dist/platforms/types.js.map +1 -0
  81. package/dist/policy/checks.js +91 -0
  82. package/dist/policy/checks.js.map +1 -0
  83. package/dist/policy/diff.js +50 -0
  84. package/dist/policy/diff.js.map +1 -0
  85. package/dist/registry/discover.js +74 -0
  86. package/dist/registry/discover.js.map +1 -0
  87. package/dist/registry/serialize.js +41 -0
  88. package/dist/registry/serialize.js.map +1 -0
  89. package/dist/registry/types.js +2 -0
  90. package/dist/registry/types.js.map +1 -0
  91. package/dist/remove/host.js +294 -0
  92. package/dist/remove/host.js.map +1 -0
  93. package/dist/render/commands.js +139 -0
  94. package/dist/render/commands.js.map +1 -0
  95. package/dist/render/manifest.js +82 -0
  96. package/dist/render/manifest.js.map +1 -0
  97. package/dist/render/markers.js +166 -0
  98. package/dist/render/markers.js.map +1 -0
  99. package/dist/render/profile.js +23 -0
  100. package/dist/render/profile.js.map +1 -0
  101. package/dist/render/standards.js +169 -0
  102. package/dist/render/standards.js.map +1 -0
  103. package/dist/render/vendors.js +199 -0
  104. package/dist/render/vendors.js.map +1 -0
  105. package/dist/review/engines/api.js +95 -0
  106. package/dist/review/engines/api.js.map +1 -0
  107. package/dist/review/engines/embedded.js +20 -0
  108. package/dist/review/engines/embedded.js.map +1 -0
  109. package/dist/review/engines/types.js +2 -0
  110. package/dist/review/engines/types.js.map +1 -0
  111. package/dist/review/glob.js +29 -0
  112. package/dist/review/glob.js.map +1 -0
  113. package/dist/review/prompt.js +45 -0
  114. package/dist/review/prompt.js.map +1 -0
  115. package/dist/review/rules.js +25 -0
  116. package/dist/review/rules.js.map +1 -0
  117. package/dist/review/schema.js +84 -0
  118. package/dist/review/schema.js.map +1 -0
  119. package/dist/review/scope.js +27 -0
  120. package/dist/review/scope.js.map +1 -0
  121. package/dist/sarif/map.js +58 -0
  122. package/dist/sarif/map.js.map +1 -0
  123. package/dist/sarif/parse.js +88 -0
  124. package/dist/sarif/parse.js.map +1 -0
  125. package/dist/sarif/types.js +2 -0
  126. package/dist/sarif/types.js.map +1 -0
  127. package/dist/sync/host.js +21 -0
  128. package/dist/sync/host.js.map +1 -0
  129. package/dist/sync/plan.js +47 -0
  130. package/dist/sync/plan.js.map +1 -0
  131. package/dist/sync/render.js +57 -0
  132. package/dist/sync/render.js.map +1 -0
  133. package/dist/sync/run.js +118 -0
  134. package/dist/sync/run.js.map +1 -0
  135. package/dist/verify/host.js +52 -0
  136. package/dist/verify/host.js.map +1 -0
  137. package/dist/verify/remote.js +146 -0
  138. package/dist/verify/remote.js.map +1 -0
  139. package/package.json +52 -0
  140. package/platforms/azure/gate-template.yml +105 -0
  141. package/rulesets/redline-org-ruleset.json +46 -0
  142. package/rulesets/redline-ruleset.json +49 -0
  143. package/scripts/assign-rule-ids.mjs +129 -0
  144. package/scripts/build-baseline.mjs +124 -0
  145. package/scripts/build-correlation.mjs +74 -0
  146. package/scripts/build-dashboard.mjs +584 -0
  147. package/scripts/build-digest.mjs +164 -0
  148. package/scripts/build-inbox.mjs +133 -0
  149. package/scripts/build-registry.mjs +42 -0
  150. package/scripts/build-roi.mjs +181 -0
  151. package/scripts/check-pins.mjs +88 -0
  152. package/scripts/collect-telemetry.mjs +300 -0
  153. package/scripts/lib/__tests__/baseline.test.mjs +132 -0
  154. package/scripts/lib/__tests__/correlate.test.mjs +131 -0
  155. package/scripts/lib/__tests__/dora.test.mjs +110 -0
  156. package/scripts/lib/__tests__/exemptions.test.mjs +85 -0
  157. package/scripts/lib/__tests__/metrics-scanner.test.mjs +65 -0
  158. package/scripts/lib/__tests__/sarif.test.mjs +125 -0
  159. package/scripts/lib/__tests__/spend.test.mjs +64 -0
  160. package/scripts/lib/baseline.mjs +156 -0
  161. package/scripts/lib/correlate.mjs +125 -0
  162. package/scripts/lib/dora.mjs +133 -0
  163. package/scripts/lib/exemptions.mjs +79 -0
  164. package/scripts/lib/metrics.mjs +172 -0
  165. package/scripts/lib/rules.mjs +107 -0
  166. package/scripts/lib/sarif.mjs +108 -0
  167. package/scripts/lib/spend.mjs +69 -0
  168. package/scripts/measure-context.mjs +101 -0
  169. package/scripts/render-self.mjs +18 -0
  170. package/scripts/score-seeds.mjs +294 -0
  171. package/scripts/validate.mjs +394 -0
  172. package/standards/core.md +124 -0
  173. package/standards/manifest.json +214 -0
  174. package/standards/stacks/csharp.md +29 -0
  175. package/standards/stacks/go.md +30 -0
  176. package/standards/stacks/java.md +30 -0
  177. package/standards/stacks/javascript.md +42 -0
  178. package/standards/stacks/kotlin.md +27 -0
  179. package/standards/stacks/microservices.md +37 -0
  180. package/standards/stacks/nodejs.md +34 -0
  181. package/standards/stacks/python.md +29 -0
  182. package/standards/stacks/react-native.md +29 -0
  183. package/standards/stacks/react.md +46 -0
  184. package/standards/stacks/swift.md +26 -0
  185. package/standards/stacks/terraform.md +27 -0
  186. package/templates/CODEOWNERS +29 -0
  187. package/templates/azure/pull_request_template.md +72 -0
  188. package/templates/github/pull_request_template.md +72 -0
  189. package/templates/redline.yml +35 -0
  190. package/templates/repo-context.md +64 -0
  191. package/workflows/dashboard.yml +119 -0
  192. package/workflows/inbox.yml +97 -0
  193. package/workflows/redline-collect.yml +61 -0
  194. package/workflows/redline-gate.yml +301 -0
  195. package/workflows/redline-sync.yml +79 -0
  196. package/workflows/seed-canary.yml +231 -0
  197. package/workflows/verify-onboarding.yml +115 -0
  198. package/workflows/weekly-digest.yml +78 -0
@@ -0,0 +1,394 @@
1
+ #!/usr/bin/env node
2
+ // Bundle self-check. This repo enforces standards on hundreds of others; it has to hold
3
+ // itself to the same bar. Run by .github/workflows/ci.yml.
4
+
5
+ import { readFileSync, existsSync, readdirSync } from 'node:fs';
6
+ import { join } from 'node:path';
7
+ import { ROOT, RANK, loadRules, parseFinding, RESERVED_RULE_IDS, RULE_ID } from './lib/rules.mjs';
8
+ const errors = [];
9
+ const warnings = [];
10
+
11
+ const fail = (msg) => errors.push(msg);
12
+ const warn = (msg) => warnings.push(msg);
13
+ const read = (rel) => readFileSync(join(ROOT, rel), 'utf8');
14
+
15
+ // --- manifest integrity -----------------------------------------------------
16
+ const manifest = JSON.parse(read('standards/manifest.json'));
17
+
18
+ if (!/^\d+\.\d+\.\d+$/.test(manifest.version)) fail(`manifest version "${manifest.version}" is not semver`);
19
+ if (!existsSync(join(ROOT, manifest.core.source))) fail(`core source missing: ${manifest.core.source}`);
20
+
21
+ for (const [id, stack] of Object.entries(manifest.stacks)) {
22
+ if (!existsSync(join(ROOT, stack.source))) fail(`stack "${id}": source missing (${stack.source})`);
23
+ if (!stack.globs?.length) fail(`stack "${id}": no globs`);
24
+ for (const glob of stack.globs ?? []) {
25
+ // Copilot's applyTo is a plain comma-separated glob list. Negation and brace
26
+ // expansion are not portable across Copilot, Cursor and AGENTS.md consumers.
27
+ if (glob.startsWith('!')) fail(`stack "${id}": negated glob "${glob}" is not supported by applyTo`);
28
+ if (glob.includes('{')) fail(`stack "${id}": brace expansion "${glob}" is not portable — list globs separately`);
29
+ if (glob.includes(',')) fail(`stack "${id}": glob "${glob}" contains a comma, which is the applyTo separator`);
30
+ }
31
+ for (const parent of stack.extends ?? []) {
32
+ if (!manifest.stacks[parent]) fail(`stack "${id}": extends unknown stack "${parent}"`);
33
+ }
34
+ }
35
+
36
+ for (const [name, stacks] of Object.entries(manifest.profiles)) {
37
+ if (!stacks.length) fail(`profile "${name}" is empty`);
38
+ for (const id of stacks) {
39
+ if (!manifest.stacks[id]) fail(`profile "${name}": unknown stack "${id}"`);
40
+ }
41
+ }
42
+
43
+ for (const [alias, target] of Object.entries(manifest.profileAliases)) {
44
+ if (!manifest.profiles[target]) fail(`profile alias "${alias}" points at unknown profile "${target}"`);
45
+ if (manifest.profiles[alias]) fail(`profile alias "${alias}" shadows a real profile`);
46
+ }
47
+
48
+ const unusedStacks = Object.keys(manifest.stacks).filter(
49
+ (id) => !Object.values(manifest.profiles).some((p) => p.includes(id))
50
+ );
51
+ if (unusedStacks.length) warn(`stacks in no profile (will never be distributed): ${unusedStacks.join(', ')}`);
52
+
53
+ // --- the severity contract has to survive edits ------------------------------
54
+ const core = read(manifest.core.source);
55
+ for (const token of ['Redline/BLOCKER', 'Redline/HIGH', 'Redline/SUGGESTION']) {
56
+ if (!core.includes(token)) fail(`core standard no longer documents "${token}" — telemetry parsing depends on it`);
57
+ }
58
+ if (!core.includes('What NOT to flag')) fail('core standard lost its "What NOT to flag" section');
59
+ if (!core.includes('[rule-id]')) fail('core standard no longer documents the rule-id part of the output contract');
60
+ for (const reserved of RESERVED_RULE_IDS) {
61
+ if (!core.includes(reserved)) fail(`core standard must document the reserved id "${reserved}"`);
62
+ }
63
+
64
+ // The parser and the documented contract must not drift apart: parse the example the
65
+ // standard itself gives and confirm it yields what the pipeline expects.
66
+ const example = parseFinding('Redline/BLOCKER [core/query-string-concatenation]: example');
67
+ if (example.severity !== 'BLOCKER' || example.ruleId !== 'core/query-string-concatenation') {
68
+ fail('the finding parser cannot parse the output contract documented in the core standard');
69
+ }
70
+
71
+ // --- rule catalogue -----------------------------------------------------------
72
+ const rules = loadRules();
73
+ if (rules.size < 200) fail(`only ${rules.size} rules carry an id — run: node scripts/assign-rule-ids.mjs`);
74
+
75
+ for (const [id, rule] of rules) {
76
+ if (!RULE_ID.test(id)) fail(`rule id "${id}" is not <stack>/<slug> in kebab-case`);
77
+ const [prefix] = id.split('/');
78
+ if (prefix !== rule.stack) {
79
+ fail(`rule "${id}" lives in ${rule.source} but is prefixed "${prefix}" instead of "${rule.stack}"`);
80
+ }
81
+ if (id.split('/')[1].length < 4) warn(`rule id "${id}" has a very short slug — it should read as a name`);
82
+ }
83
+
84
+ // Every normative bullet must carry an id, or telemetry silently loses it.
85
+ const idsAssigned = readdirSync(join(ROOT, 'standards/stacks'))
86
+ .map((f) => `standards/stacks/${f}`)
87
+ .concat(['standards/core.md']);
88
+ for (const relPath of idsAssigned) {
89
+ const lines = read(relPath).split('\n');
90
+ let inRules = false;
91
+ let inFence = false;
92
+ lines.forEach((line, i) => {
93
+ if (/^```/.test(line.trim())) inFence = !inFence;
94
+ if (inFence) return;
95
+ if (/^#{1,6}\s/.test(line)) {
96
+ inRules = /^#{2,3}\s+(BLOCKER|HIGH|SUGGESTION|Security|Type safety|Error handling|General correctness|Scope discipline)\b/i.test(line);
97
+ return;
98
+ }
99
+ if (!inRules || !/^-\s+/.test(line)) return;
100
+ if (!/^-\s+`[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*`\s+—\s/.test(line)) {
101
+ fail(`${relPath}:${i + 1} is a rule with no id — run: node scripts/assign-rule-ids.mjs`);
102
+ }
103
+ });
104
+ }
105
+
106
+ // --- rulesets ----------------------------------------------------------------
107
+ const REQUIRED_CHECK = 'redline-gate / gate';
108
+ for (const file of ['rulesets/redline-ruleset.json', 'rulesets/redline-org-ruleset.json']) {
109
+ const ruleset = JSON.parse(read(file));
110
+ const checks = ruleset.rules
111
+ .filter((r) => r.type === 'required_status_checks')
112
+ .flatMap((r) => r.parameters.required_status_checks.map((c) => c.context));
113
+ if (!checks.includes(REQUIRED_CHECK)) {
114
+ fail(`${file}: required status check is ${JSON.stringify(checks)}, expected "${REQUIRED_CHECK}"`);
115
+ }
116
+ const pr = ruleset.rules.find((r) => r.type === 'pull_request');
117
+ if (!pr) fail(`${file}: no pull_request rule — human approval is not enforced`);
118
+ else if ((pr.parameters.required_approving_review_count ?? 0) < 1) {
119
+ fail(`${file}: required_approving_review_count must be at least 1 — automated review never approves`);
120
+ }
121
+ if (ruleset.bypass_actors?.length) warn(`${file}: has ${ruleset.bypass_actors.length} bypass actor(s)`);
122
+ }
123
+
124
+ // --- workflows ---------------------------------------------------------------
125
+ const callerTemplate = read('templates/redline.yml');
126
+ if (!/^\s{2}redline-gate:\s*$/m.test(callerTemplate)) {
127
+ fail('templates/redline.yml: the caller job id must be `redline-gate` — the required check name derives from it');
128
+ }
129
+
130
+ const gate = read('workflows/redline-gate.yml');
131
+ if (!/^\s{2}gate:\s*$/m.test(gate)) {
132
+ fail('workflows/redline-gate.yml: the aggregate job id must be `gate` — the required check name derives from it');
133
+ }
134
+ if (!gate.includes('pull-requests: write')) {
135
+ fail('workflows/redline-gate.yml: dependency-review needs pull-requests: write to comment');
136
+ }
137
+
138
+ // --- the derived register -----------------------------------------------------
139
+ // The register is the estate's only source of truth for which repositories are
140
+ // onboarded. It was lost once already — scripts/setup-repo.sh was its only writer
141
+ // and sync-targets.txt went with it — and nothing failed, which is why the dashboard
142
+ // quietly stopped reporting coverage. These two assertions are what makes that
143
+ // silent again impossible.
144
+ if (!existsSync(join(ROOT, 'scripts/build-registry.mjs'))) {
145
+ fail('scripts/build-registry.mjs is missing — the register cannot be derived, so redline sync has no targets and the dashboard loses its coverage figure');
146
+ }
147
+
148
+ // --- the estate command surface -------------------------------------------------
149
+ // Every runner the metrics commands dispatch to must ship, because the commands
150
+ // locate them inside the installed package rather than in a checkout. A runner
151
+ // missing from package.json "files" is an installation where `redline metrics
152
+ // dashboard` exists, is documented, and cannot run — the exact broken promise the
153
+ // command surface was built to remove.
154
+ const optionsSource = read('cli/metrics/options.ts');
155
+ for (const match of optionsSource.matchAll(/script: '([^']+)'/g)) {
156
+ if (!existsSync(join(ROOT, match[1]))) {
157
+ fail(`cli/metrics/options.ts points at ${match[1]}, which does not exist`);
158
+ }
159
+ }
160
+ const shippedPaths = JSON.parse(read('package.json')).files ?? [];
161
+ if (!shippedPaths.includes('scripts/')) {
162
+ fail('package.json "files" no longer ships scripts/ — every redline metrics command would be present, documented, and unable to run');
163
+ }
164
+ if (!existsSync(join(ROOT, '.github/workflows/registry.yml'))) {
165
+ fail('.github/workflows/registry.yml is missing — the register would silently stop refreshing and go stale without a single failing build');
166
+ }
167
+
168
+ // --- distribution -------------------------------------------------------------
169
+ // Sync is what makes a standards change reach the estate. Without it every rule
170
+ // change here is a change nobody receives, and the failure is silent by nature:
171
+ // the repositories that did not get it look exactly like the ones that did.
172
+ const syncWorkflow = read('workflows/redline-sync.yml');
173
+ if (/^\s*if:\s*false\s*$/m.test(syncWorkflow)) {
174
+ fail('workflows/redline-sync.yml is gated off — a standards change would reach no onboarded repository');
175
+ }
176
+ if (!syncWorkflow.includes('redline.js sync')) {
177
+ fail('workflows/redline-sync.yml no longer calls `redline sync` — distribution is wired to nothing');
178
+ }
179
+
180
+ // Drift re-detection is the other half of distribution: sync makes a change
181
+ // available, this is what notices a repository that never took it, or that had
182
+ // its gate quietly weakened afterwards.
183
+ const verifyWorkflow = read('workflows/verify-onboarding.yml');
184
+ if (/^\s*if:\s*false\s*$/m.test(verifyWorkflow)) {
185
+ fail('workflows/verify-onboarding.yml is gated off — estate drift would go undetected');
186
+ }
187
+ if (!verifyWorkflow.includes('verify --repo')) {
188
+ fail('workflows/verify-onboarding.yml no longer calls `redline verify --repo` — drift detection is wired to nothing');
189
+ }
190
+
191
+ // The baseline is what every roadmap phase past 0 is judged against, and the
192
+ // roadmap says plainly that the ordering of the later phases is a hypothesis
193
+ // until it exists. Losing the instrument loses the ability to tell whether any
194
+ // of them worked.
195
+ if (!existsSync(join(ROOT, 'scripts/build-baseline.mjs'))) {
196
+ fail('scripts/build-baseline.mjs is missing — there is no way to compute the baseline every later roadmap phase is measured against');
197
+ }
198
+ if (!existsSync(join(ROOT, 'scripts/build-roi.mjs'))) {
199
+ fail('scripts/build-roi.mjs is missing — the one page that says what AI review cost against what it caught');
200
+ }
201
+
202
+ // --- exemptions ---------------------------------------------------------------
203
+ // The exemption block is parsed twice, deliberately: the CLI enforces it at the
204
+ // gate, and the collector reads it for telemetry in the metrics repo where there
205
+ // is no build step to import dist/ from. The failure this guards is the gate
206
+ // accepting a block the audit cannot read — an exemption enforced and then never
207
+ // reported, which is precisely the state F exists to end.
208
+ const exemptCli = read('cli/exempt/parse.ts');
209
+ const exemptCollector = read('scripts/lib/exemptions.mjs');
210
+ for (const token of ['## Redline exemption', 'reason', 'until', 'scope']) {
211
+ if (!exemptCli.includes(token) || !exemptCollector.includes(token)) {
212
+ fail(`the exemption block's "${token}" is missing from cli/exempt/parse.ts or scripts/lib/exemptions.mjs — the gate and the audit would read different blocks`);
213
+ }
214
+ }
215
+ if (!read('.github/pull_request_template.md').includes('Redline exemption')) {
216
+ fail('.github/pull_request_template.md does not mention the exemption block — an author asked to justify a waiver has nowhere to write it');
217
+ }
218
+
219
+ // --- ingestion ----------------------------------------------------------------
220
+ // Same two-implementation arrangement as the exemption block, and the same guard.
221
+ // The property being protected is different and larger: an ingested finding must
222
+ // always be distinguishable from a Redline one. A collector that stopped tagging
223
+ // the source would tune Redline's rules on another tool's noise, silently, and
224
+ // nothing else in the system would notice.
225
+ const sarifCli = read('cli/sarif/types.ts');
226
+ const sarifCollector = read('scripts/lib/sarif.mjs');
227
+ for (const token of ["'sarif'", 'severity']) {
228
+ if (!sarifCli.includes(token) || !sarifCollector.includes(token)) {
229
+ fail(`ingestion lost "${token}" from cli/sarif/ or scripts/lib/sarif.mjs — an ingested finding must always be distinguishable from a Redline one`);
230
+ }
231
+ }
232
+ if (!read('scripts/lib/metrics.mjs').includes('scanner')) {
233
+ fail('scripts/lib/metrics.mjs no longer aggregates ingested findings separately — they would be folded into Redline\'s own rule tuning');
234
+ }
235
+ if (read('workflows/redline-gate.yml').includes('code-scanning')) {
236
+ fail('workflows/redline-gate.yml reads code scanning — ingested findings are measured only and must never gate a merge (roadmap open question 4)');
237
+ }
238
+
239
+ // --- the deterministic tier ----------------------------------------------------
240
+ // A rule classified as machine-checked and then checked by nobody is worse than
241
+ // one left to the model: everybody believes it is covered, and the model was told
242
+ // nothing, so it is enforced by no one at all.
243
+ const deterministic = manifest.deterministic ?? [];
244
+ const checksSource = read('cli/policy/checks.ts');
245
+ for (const id of deterministic) {
246
+ if (!rules.has(id)) {
247
+ fail(`manifest "deterministic" lists "${id}", which is not a rule in standards/ — ids are permanent and this one does not exist`);
248
+ }
249
+ if (!checksSource.includes(`'${id}'`)) {
250
+ fail(`rule "${id}" is classified deterministic but cli/policy/checks.ts implements no check for it — it is enforced by nobody`);
251
+ }
252
+ }
253
+
254
+ // --- local review ---------------------------------------------------------------
255
+ // A local review is opt-in and enforces nothing; the pull request review is the
256
+ // system of record. Its findings must never reach the telemetry that tunes rules,
257
+ // or acted-on rate is computed partly from runs nobody can verify — a local run
258
+ // has no thread to resolve, no reviewer to attribute, and no way to tell a fixed
259
+ // finding from one the author never read.
260
+ if (!read('cli/commands/review.ts').includes('excludedFromTelemetry')) {
261
+ fail('cli/commands/review.ts no longer marks local reviews as excluded from telemetry — local findings would distort acted-on rate');
262
+ }
263
+
264
+ // --- pull request template ----------------------------------------------------
265
+ // Two copies of one file, deliberately, kept identical by this check.
266
+ //
267
+ // templates/github/pull_request_template.md is the shipped source: package.json
268
+ // "files" packages templates/, and `redline init` installs it into an onboarded
269
+ // repository. It cannot live under .github/, because .github/ is excluded from the
270
+ // tarball on purpose — packaging it would push this repo's own CI workflows into
271
+ // every consumer.
272
+ //
273
+ // .github/pull_request_template.md is this repository's own copy. Redline is
274
+ // onboarded to Redline, and the `checklist` job in workflows/redline-gate.yml reads
275
+ // the pull request *body*, which GitHub pre-fills from that path. Without it every
276
+ // pull request opened here starts empty and fails this repo's own gate.
277
+ //
278
+ // Neither file carries an explanatory comment of its own: the shipped one is copied
279
+ // verbatim into every consumer's PR body, so a note about Redline's packaging would
280
+ // end up in other teams' pull requests. The explanation lives here, where the drift
281
+ // it guards against is caught.
282
+ const GATED_SECTION = '## Launch readiness';
283
+ const shippedTemplate = read('templates/github/pull_request_template.md');
284
+ const ownTemplate = existsSync(join(ROOT, '.github/pull_request_template.md'))
285
+ ? read('.github/pull_request_template.md')
286
+ : null;
287
+
288
+ if (ownTemplate === null) {
289
+ fail('.github/pull_request_template.md is missing — every pull request here would start with an empty body and fail the checklist job in workflows/redline-gate.yml');
290
+ } else if (ownTemplate !== shippedTemplate) {
291
+ fail('.github/pull_request_template.md has drifted from templates/github/pull_request_template.md — the shipped template is the source, copy it across');
292
+ }
293
+ if (!shippedTemplate.includes(GATED_SECTION)) {
294
+ fail(`templates/github/pull_request_template.md: no "${GATED_SECTION}" section — workflows/redline-gate.yml fails the checklist job without it`);
295
+ }
296
+
297
+ const workflowFiles = [
298
+ ...readdirSync(join(ROOT, 'workflows')).map((f) => `workflows/${f}`),
299
+ ...(existsSync(join(ROOT, '.github/workflows'))
300
+ ? readdirSync(join(ROOT, '.github/workflows')).map((f) => `.github/workflows/${f}`)
301
+ : []),
302
+ 'templates/redline.yml',
303
+ ].filter((f) => f.endsWith('.yml') || f.endsWith('.yaml'));
304
+
305
+ for (const file of workflowFiles) {
306
+ const body = read(file);
307
+ for (const [, action] of body.matchAll(/^\s*-?\s*uses:\s*([^\s#]+)\s*(?:#.*)?$/gm)) {
308
+ if (action.startsWith('<org>/') || action.startsWith('./')) continue;
309
+ if (/@(main|master|latest)$/.test(action)) {
310
+ // The org's own reusable workflow is a first-party ref and is allowed to float.
311
+ if (action.includes('/.github/.github/workflows/')) continue;
312
+ fail(`${file}: third-party action pinned to a mutable ref: ${action}`);
313
+ }
314
+ const [owner] = action.split('/');
315
+ if (owner !== 'actions' && !/@[0-9a-f]{40}$/.test(action)) {
316
+ warn(`${file}: ${action} is not pinned to a commit SHA`);
317
+ }
318
+ }
319
+ // Expressions are not allowed in a step-level `uses:`; the workflow silently fails to parse.
320
+ for (const [, action] of body.matchAll(/^\s+uses:\s*(\$\{\{[^\n]*)/gm)) {
321
+ if (!/^\s*jobs:/m.test(action)) fail(`${file}: expression in a step \`uses:\` is not supported by Actions: ${action.trim()}`);
322
+ }
323
+ }
324
+
325
+ // --- seeds --------------------------------------------------------------------
326
+ // Recall without precision is meaningless, so both corpora are load-bearing and both
327
+ // are enforced here rather than left to a reviewer to notice.
328
+ const SEED_MARKER = /SEED\s+(\d+)\s*\[(BLOCKER|HIGH|SUGGESTION)\]\s*(?:\(([^)]+)\))?/;
329
+ const MIN_BLOCKERS_PER_STACK = 4;
330
+ const seedDir = join(ROOT, 'seeded');
331
+ const seedDirs = readdirSync(seedDir, { withFileTypes: true })
332
+ .filter((e) => e.isDirectory())
333
+ .map((e) => e.name);
334
+
335
+ const seedFiles = (dir) =>
336
+ readdirSync(join(seedDir, dir)).filter((f) => !f.endsWith('.md'));
337
+
338
+ for (const id of Object.keys(manifest.stacks)) {
339
+ if (!seedDirs.includes(id)) {
340
+ fail(`seeded/${id}/ is missing — that stack's catch rate cannot be measured`);
341
+ continue;
342
+ }
343
+ const files = seedFiles(id);
344
+ if (!files.length) {
345
+ fail(`seeded/${id}/ has no seed file`);
346
+ continue;
347
+ }
348
+ let blockers = 0;
349
+ let ids = new Set();
350
+ for (const file of files) {
351
+ for (const line of readFileSync(join(seedDir, id, file), 'utf8').split('\n')) {
352
+ const m = SEED_MARKER.exec(line);
353
+ if (!m) continue;
354
+ const key = `${file}#${m[1]}`;
355
+ if (ids.has(key)) fail(`seeded/${id}/${file}: duplicate SEED ${m[1]}`);
356
+ ids.add(key);
357
+ if (m[2] === 'BLOCKER') blockers += 1;
358
+
359
+ // A seed without a rule id cannot verify attribution; one citing an unknown or
360
+ // weaker rule silently makes the corpus disagree with the standard it tests.
361
+ const cited = m[3];
362
+ if (!cited) {
363
+ fail(`seeded/${id}/${file}: SEED ${m[1]} does not cite a rule id`);
364
+ } else if (!rules.has(cited) && !RESERVED_RULE_IDS.has(cited)) {
365
+ fail(`seeded/${id}/${file}: SEED ${m[1]} cites unknown rule "${cited}"`);
366
+ } else if (rules.has(cited) && RANK[m[2]] > RANK[rules.get(cited).severity]) {
367
+ fail(
368
+ `seeded/${id}/${file}: SEED ${m[1]} expects ${m[2]} but rule "${cited}" is ${rules.get(cited).severity} in the standard`
369
+ );
370
+ }
371
+ }
372
+ }
373
+ if (blockers < MIN_BLOCKERS_PER_STACK) {
374
+ fail(`seeded/${id}/ has ${blockers} BLOCKER seed(s), minimum is ${MIN_BLOCKERS_PER_STACK}`);
375
+ }
376
+ }
377
+
378
+ if (!seedDirs.includes('clean')) {
379
+ fail('seeded/clean is missing — recall is measured but precision is not');
380
+ } else {
381
+ const clean = seedFiles('clean');
382
+ if (!clean.length) fail('seeded/clean is empty — precision is unmeasured');
383
+ for (const file of clean) {
384
+ if (SEED_MARKER.test(readFileSync(join(seedDir, 'clean', file), 'utf8'))) {
385
+ fail(`seeded/clean/${file} contains a SEED marker — the precision corpus must be defect-free`);
386
+ }
387
+ }
388
+ }
389
+
390
+ // --- report -------------------------------------------------------------------
391
+ warnings.forEach((w) => console.warn(`warn: ${w}`));
392
+ errors.forEach((e) => console.error(`FAIL: ${e}`));
393
+ console.log(`\n${errors.length} error(s), ${warnings.length} warning(s)`);
394
+ process.exit(errors.length ? 1 : 0);
@@ -0,0 +1,124 @@
1
+ # Redline — Core Engineering Standards
2
+
3
+ You are performing code review against Redline, the engineering standard for this
4
+ organisation. Stack-specific rules extend these; they never override them.
5
+
6
+ ## Output contract (required)
7
+
8
+ Every finding you post MUST begin with a machine-readable prefix on its own first line:
9
+
10
+ ```
11
+ Redline/BLOCKER [rule-id]: <one-line problem>
12
+ Redline/HIGH [rule-id]: <one-line problem>
13
+ Redline/SUGGESTION [rule-id]: <one-line problem>
14
+ ```
15
+
16
+ Then one or two sentences: why it breaks, and the concrete fix. No preamble, no praise,
17
+ no restating the diff. One finding per comment. If nothing qualifies, post nothing.
18
+
19
+ Worked example:
20
+
21
+ ```
22
+ Redline/BLOCKER [core/query-string-concatenation]: user-supplied `name` is concatenated
23
+ into the SQL string, so a crafted value changes the query.
24
+ Use a parameterised query: `db.Query("SELECT id FROM users WHERE name = $1", name)`.
25
+ ```
26
+
27
+ ### Rule ids
28
+
29
+ Every rule in this document and in the stack rules carries an id in backticks at the
30
+ start of its line, of the form `<stack>/<slug>` — for example `react/effect-derived-state`.
31
+
32
+ - Quote the id of the rule you are applying, exactly as written. Do not invent, abbreviate,
33
+ pluralise, or reformat it.
34
+ - One rule per comment. If a line breaks two rules, post two comments.
35
+ - If you are confident something is wrong but no rule covers it, use `core/uncatalogued`
36
+ and say plainly which principle it offends. A recurring `core/uncatalogued` is how a
37
+ missing rule gets discovered, so do not force a bad match to avoid it.
38
+
39
+ Ids are stable across wording changes and are aggregated per rule, which is how the org
40
+ finds out which rules earn their place and which only generate noise. A finding without a
41
+ valid id cannot be measured and is treated as untagged.
42
+
43
+ Severity meaning:
44
+
45
+ - **BLOCKER** — must not merge. Security exposure, data loss, crash, silent corruption,
46
+ or a contract break for live consumers.
47
+ - **HIGH** — merge is a deliberate trade-off. Reviewer must acknowledge explicitly.
48
+ - **SUGGESTION** — optional. Author may dismiss without justification.
49
+
50
+ Do not invent severities. Do not upgrade a SUGGESTION to HIGH to get attention.
51
+
52
+ ## Review priorities (in order)
53
+
54
+ 1. Security and data exposure
55
+ 2. Correctness bugs
56
+ 3. Type safety
57
+ 4. Performance regressions
58
+ 5. Maintainability
59
+
60
+ Stop at the first three unless the diff is clean there.
61
+
62
+ ## Security (BLOCKER)
63
+
64
+ - `core/hardcoded-secrets` — No hardcoded secrets, API keys, tokens, or credentials — including in test files,
65
+ fixtures, config samples, and comments.
66
+ - `core/customer-data-in-logs` — No customer data (MSISDN, email, account IDs, names, addresses) in logs, analytics
67
+ events, error messages, or metric labels.
68
+ - `core/unvalidated-boundary-input` — All external input validated at system boundaries (forms, API responses, deep links,
69
+ query params, webhook payloads, message-queue payloads).
70
+ - `core/html-injection-sink` — No unsanitised HTML injection sinks (`dangerouslySetInnerHTML`, `innerHTML`, template
71
+ autoescape disabled).
72
+ - `core/missing-auth-check` — Auth checks on every server action / API route / service endpoint — not only in the UI
73
+ layer or at the gateway.
74
+ - `core/sensitive-data-in-client-storage` — No sensitive data in browser or device storage (`localStorage`, `AsyncStorage`,
75
+ `UserDefaults`, `SharedPreferences`) without platform-keystore encryption.
76
+ - `core/query-string-concatenation` — No query built by string concatenation with external input — parameterised only.
77
+ - `core/secrets-in-committed-config` — No secrets read from committed config — environment or vault only.
78
+
79
+ ## Type safety (BLOCKER unless justified inline)
80
+
81
+ - `core/escape-hatch-types` — No escape-hatch types (`any`, `interface{}` in new Go code, `Object`, `dynamic`)
82
+ where a concrete or generic type works.
83
+ - `core/type-checker-suppression` — No type-checker suppression (`@ts-ignore`, `@ts-expect-error`, `# type: ignore`,
84
+ `@SuppressWarnings("unchecked")`) without an inline comment AND a ticket reference.
85
+ - `core/unsafe-assertion` — No unsafe assertions (`as unknown as X`, force casts) used to silence an error.
86
+ - `core/prefer-discriminated-unions` — Discriminated unions / sealed types over optional-field grab-bags for variant state.
87
+ - `core/unchecked-indexed-access` — Assume the strictest project setting is on (TS `strict` + `noUncheckedIndexedAccess`,
88
+ Kotlin/Swift null-safety, mypy strict): indexed access may be absent — require the check.
89
+
90
+ ## Error handling
91
+
92
+ - `core/missing-boundary-error-handling` — Error handling belongs at real system boundaries: user input, network calls, storage,
93
+ native modules, message consumers. Flag missing handling there.
94
+ - `core/unreachable-defensive-guard` — Flag defensive guards against states internal code cannot produce — they hide bugs and
95
+ add noise.
96
+ - `core/silent-async-failure` — Async operations that can reject must not fail silently: no empty catch, no floating
97
+ promises, no error logged then treated as success.
98
+
99
+ ## General correctness
100
+
101
+ - `core/argument-mutation` — Flag mutation of function arguments or shared objects.
102
+ - `core/async-race-condition` — Flag race conditions in async work: missing cancellation/abort when the owner unmounts,
103
+ the request is superseded, or the context is cancelled.
104
+ - `core/untracked-todo` — Flag `TODO`/`FIXME`/placeholder code without a ticket reference.
105
+ - `core/naive-clock` — Flag time handling that assumes local timezone or a naive clock in new code.
106
+
107
+ ## Scope discipline
108
+
109
+ - `core/unrelated-change` — Flag changes unrelated to the PR's stated purpose (drive-by refactors, formatting churn).
110
+ - `core/speculative-abstraction` — Prefer the minimal diff that solves the problem; flag speculative abstraction
111
+ ("might need it later").
112
+
113
+ ## What NOT to flag
114
+
115
+ AI review dies by nitpick spam. Noise control is a rule, not a preference.
116
+
117
+ - Formatting, import order, or anything a linter or formatter already enforces.
118
+ - Existing patterns the PR merely touches but does not change.
119
+ - Missing tests for code outside the diff.
120
+ - Alternative libraries when the current one works ("consider using X instead").
121
+ - Naming preferences where the existing name is unambiguous.
122
+ - Re-raising the same issue on every occurrence — flag the first, say "and N similar".
123
+ - Anything you cannot point at a concrete failure for. If you cannot describe the input
124
+ that breaks it, it is not a finding.