hstack 0.16.0 → 0.18.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.
- package/CHANGELOG.md +53 -0
- package/README.md +76 -163
- package/VERSION +1 -1
- package/dist/commands/doctor.js +18 -8
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.js +7 -3
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/update.js +9 -2
- package/dist/commands/update.js.map +1 -1
- package/dist/lib/descriptions.js +10 -17
- package/dist/lib/descriptions.js.map +1 -1
- package/dist/lib/wire.js +166 -138
- package/dist/lib/wire.js.map +1 -1
- package/dist/manifest.js +41 -29
- package/dist/manifest.js.map +1 -1
- package/package.json +3 -4
- package/template/.claude/agents/adversarial-reviewer.md +44 -50
- package/template/.claude/agents/app-architect.md +44 -96
- package/template/.claude/agents/data-architect.md +46 -89
- package/template/.claude/agents/test-strategist.md +39 -68
- package/template/.claude/skills/hstack-adr-new/SKILL.md +23 -73
- package/template/.claude/skills/hstack-adversarial-review/SKILL.md +46 -152
- package/template/.claude/skills/hstack-adversarial-review/references/finding-categories.md +89 -103
- package/template/.claude/skills/hstack-app-architecture/SKILL.md +43 -68
- package/template/.claude/skills/hstack-commit/SKILL.md +27 -68
- package/template/.claude/skills/hstack-data-architecture/SKILL.md +34 -56
- package/template/.claude/skills/hstack-promote/SKILL.md +55 -0
- package/template/.claude/skills/hstack-story/SKILL.md +58 -0
- package/template/.claude/skills/hstack-test-audit/SKILL.md +64 -0
- package/template/.claude/skills/hstack-wrap/SKILL.md +50 -0
- package/template/.claude/skills/hstack-wrap/references/pr-description.md +45 -0
- package/template/KERNEL.md +75 -318
- package/template/templates/adr.md +2 -18
- package/template/templates/app-architecture.md +38 -50
- package/template/templates/code-standards.md +107 -0
- package/template/templates/data-architecture.md +38 -89
- package/template/templates/eslint-clean-code.mjs +322 -0
- package/template/templates/infrastructure.md +54 -148
- package/template/templates/roadmap.md +6 -12
- package/template/templates/story.md +7 -13
- package/template/templates/tech-debt.md +2 -37
- package/template/templates/tech-stack.md +2 -12
- package/template/.claude/agents/data-specialist.md +0 -81
- package/template/.claude/agents/implementer.md +0 -97
- package/template/.claude/agents/kernel-fit-analyst.md +0 -129
- package/template/.claude/agents/planner.md +0 -75
- package/template/.claude/agents/product-discovery.md +0 -129
- package/template/.claude/agents/product-manager.md +0 -76
- package/template/.claude/agents/researcher.md +0 -87
- package/template/.claude/agents/security-reviewer.md +0 -88
- package/template/.claude/agents/spec-author.md +0 -89
- package/template/.claude/agents/stack-architect.md +0 -108
- package/template/.claude/agents/ui-ux-briefer.md +0 -75
- package/template/.claude/agents/verifier.md +0 -81
- package/template/.claude/skills/hstack-branch/SKILL.md +0 -76
- package/template/.claude/skills/hstack-brownfield-init/SKILL.md +0 -131
- package/template/.claude/skills/hstack-change-new/SKILL.md +0 -84
- package/template/.claude/skills/hstack-change-plan/SKILL.md +0 -108
- package/template/.claude/skills/hstack-configure/SKILL.md +0 -99
- package/template/.claude/skills/hstack-coord/SKILL.md +0 -95
- package/template/.claude/skills/hstack-data-review/SKILL.md +0 -93
- package/template/.claude/skills/hstack-finalize/SKILL.md +0 -140
- package/template/.claude/skills/hstack-flag/SKILL.md +0 -99
- package/template/.claude/skills/hstack-greenfield-init/SKILL.md +0 -148
- package/template/.claude/skills/hstack-help/SKILL.md +0 -153
- package/template/.claude/skills/hstack-implement/SKILL.md +0 -155
- package/template/.claude/skills/hstack-kernel-fit-promote/SKILL.md +0 -125
- package/template/.claude/skills/hstack-kernel-fit-scan/SKILL.md +0 -142
- package/template/.claude/skills/hstack-kernel-fit-scan/references/slack-setup.md +0 -42
- package/template/.claude/skills/hstack-kernel-fit-triage/SKILL.md +0 -121
- package/template/.claude/skills/hstack-module-spec/SKILL.md +0 -78
- package/template/.claude/skills/hstack-product-discovery/SKILL.md +0 -75
- package/template/.claude/skills/hstack-research/SKILL.md +0 -115
- package/template/.claude/skills/hstack-scaffold/SKILL.md +0 -113
- package/template/.claude/skills/hstack-security-review/SKILL.md +0 -95
- package/template/.claude/skills/hstack-ship/SKILL.md +0 -119
- package/template/.claude/skills/hstack-stack-decide/SKILL.md +0 -81
- package/template/.claude/skills/hstack-story-draft/SKILL.md +0 -92
- package/template/.claude/skills/hstack-tech-debt-new/SKILL.md +0 -95
- package/template/.claude/skills/hstack-tech-debt-resolve/SKILL.md +0 -123
- package/template/.claude/skills/hstack-tech-debt-stale/SKILL.md +0 -86
- package/template/.claude/skills/hstack-tech-debt-wontfix/SKILL.md +0 -77
- package/template/.claude/skills/hstack-telemetry/SKILL.md +0 -73
- package/template/.claude/skills/hstack-test-plan/SKILL.md +0 -159
- package/template/.claude/skills/hstack-ui-brief/SKILL.md +0 -81
- package/template/.claude/skills/hstack-verify/SKILL.md +0 -128
- package/template/scripts/compute-merge-readiness.mjs +0 -780
- package/template/scripts/coord/coord_scan.py +0 -681
- package/template/scripts/run-gates.sh +0 -388
- package/template/scripts/telemetry/__init__.py +0 -6
- package/template/scripts/telemetry/insights/__init__.py +0 -0
- package/template/scripts/telemetry/insights/contract_drift.py +0 -137
- package/template/scripts/telemetry/insights/kernel_fit.py +0 -438
- package/template/scripts/telemetry/insights/overengineering.py +0 -115
- package/template/scripts/telemetry/insights/quality_outcomes.py +0 -131
- package/template/scripts/telemetry/insights/token_economics.py +0 -302
- package/template/scripts/telemetry/insights/workflow_shape.py +0 -198
- package/template/scripts/telemetry/parsers/__init__.py +0 -0
- package/template/scripts/telemetry/parsers/bodies.py +0 -87
- package/template/scripts/telemetry/parsers/commits.py +0 -219
- package/template/scripts/telemetry/parsers/frontmatter.py +0 -322
- package/template/scripts/telemetry/parsers/sidecars.py +0 -61
- package/template/scripts/telemetry/parsers/transcripts.py +0 -294
- package/template/scripts/telemetry/render.py +0 -460
- package/template/scripts/telemetry/report.py +0 -150
- package/template/scripts/telemetry/run_kernel_fit.py +0 -95
- package/template/scripts/telemetry/session_id.py +0 -139
- package/template/scripts/validate-spec.mjs +0 -3303
- package/template/templates/adversarial-review.md +0 -54
- package/template/templates/bootstrap.md +0 -105
- package/template/templates/change-spec.md +0 -82
- package/template/templates/ci-cd.md +0 -41
- package/template/templates/coord-message.md +0 -56
- package/template/templates/data-review.md +0 -54
- package/template/templates/discovery/brainstorm.md +0 -69
- package/template/templates/discovery/forcing-questions.md +0 -76
- package/template/templates/discovery/project-brief.md +0 -53
- package/template/templates/figma-handoff.md +0 -38
- package/template/templates/glossary.md +0 -20
- package/template/templates/hardening-checklist.md +0 -73
- package/template/templates/incident-runbook.md +0 -57
- package/template/templates/kernel-fit-finding.md +0 -63
- package/template/templates/kernel-fit-flag.md +0 -52
- package/template/templates/module-spec.md +0 -49
- package/template/templates/persona.md +0 -38
- package/template/templates/plan.md +0 -53
- package/template/templates/product-brief.md +0 -56
- package/template/templates/security-review.md +0 -63
- package/template/templates/telemetry-sidecar.md +0 -227
- package/template/templates/test-plan.md +0 -119
- package/template/templates/threat-model.md +0 -54
- package/template/templates/ui-brief.md +0 -49
- package/template/templates/verification.md +0 -63
- package/template/templates/vision.md +0 -34
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hstack — clean-code ESLint reference config.
|
|
3
|
+
*
|
|
4
|
+
* The mechanical half of `hstack/context/code-standards.md`: everything a
|
|
5
|
+
* linter can measure lives here, at `error`, so `lint` fails instead of
|
|
6
|
+
* warning. A `warn` is a message an agent learns to scroll past; an `error`
|
|
7
|
+
* is a message it has to act on. Nothing here is a style preference — each
|
|
8
|
+
* rule maps to a way agent-written code has actually gone wrong.
|
|
9
|
+
*
|
|
10
|
+
* Usage, from the consumer's `eslint.config.mjs`:
|
|
11
|
+
*
|
|
12
|
+
* import { defineConfig } from 'eslint/config'
|
|
13
|
+
* import { cleanCode } from './hstack/templates/eslint-clean-code.mjs'
|
|
14
|
+
*
|
|
15
|
+
* export default defineConfig([
|
|
16
|
+
* ...yourFrameworkConfig, // e.g. eslint-config-next, which registers
|
|
17
|
+
* // the @typescript-eslint and import plugins
|
|
18
|
+
* ...cleanCode({
|
|
19
|
+
* typescript: true, // rules that need @typescript-eslint registered
|
|
20
|
+
* imports: true, // rules that need eslint-plugin-import registered
|
|
21
|
+
* supabase: true, // the unread-`error` rule (see below)
|
|
22
|
+
* modules: {
|
|
23
|
+
* logger: 'lib/observability/logger',
|
|
24
|
+
* config: 'lib/env',
|
|
25
|
+
* dbClient: 'lib/supabase',
|
|
26
|
+
* },
|
|
27
|
+
* }),
|
|
28
|
+
* ])
|
|
29
|
+
*
|
|
30
|
+
* The ratchet. Turning these on against an existing codebase fails `lint`
|
|
31
|
+
* hundreds of times at once. Freeze the current violations once —
|
|
32
|
+
*
|
|
33
|
+
* npx eslint --suppress-all
|
|
34
|
+
*
|
|
35
|
+
* — which writes `eslint-suppressions.json`. From then on only *new*
|
|
36
|
+
* violations fail; touching a file that carries old ones is the moment to
|
|
37
|
+
* pay them down (`npx eslint --prune-suppressions` drops the entries that no
|
|
38
|
+
* longer apply). Commit the suppressions file. The count only goes down.
|
|
39
|
+
*
|
|
40
|
+
* This file is framework-owned: `hstack update` overwrites it. Adjust
|
|
41
|
+
* thresholds through the `thresholds` option, not by editing here.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
const DEFAULT_FILES = ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']
|
|
45
|
+
const DEFAULT_TEST_FILES = [
|
|
46
|
+
'**/*.test.{ts,tsx,js,jsx}',
|
|
47
|
+
'**/*.spec.{ts,tsx,js,jsx}',
|
|
48
|
+
'**/__tests__/**',
|
|
49
|
+
'**/e2e/**',
|
|
50
|
+
]
|
|
51
|
+
const DEFAULT_SCRIPT_FILES = ['scripts/**']
|
|
52
|
+
|
|
53
|
+
const DEFAULT_THRESHOLDS = {
|
|
54
|
+
/** Lines per function, blank lines and comments excluded. */
|
|
55
|
+
functionLines: 60,
|
|
56
|
+
/** Lines per file, blank lines and comments excluded. */
|
|
57
|
+
fileLines: 400,
|
|
58
|
+
/** Independent paths through a function (if / for / && / ?: each add one). */
|
|
59
|
+
complexity: 15,
|
|
60
|
+
/** Nested blocks. Four is already a function that wants splitting. */
|
|
61
|
+
depth: 4,
|
|
62
|
+
/** Positional parameters. Past three, pass a named object. */
|
|
63
|
+
params: 3,
|
|
64
|
+
/** Nested callbacks. */
|
|
65
|
+
callbacks: 3,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Supabase returns `{ data, error }` and never throws. Code that takes `data`
|
|
70
|
+
* and ignores `error` reads as correct and fails silently — an RLS denial
|
|
71
|
+
* becomes an HTTP 200 that lies. This rule flags an awaited query-builder
|
|
72
|
+
* chain (`.from(…)`, `.rpc(…)`, `.storage.from(…)`) whose result is
|
|
73
|
+
* discarded, destructured without `error`, or read through `.data` only.
|
|
74
|
+
* Chaining `.throwOnError()` turns the result into an exception and satisfies
|
|
75
|
+
* the rule.
|
|
76
|
+
*/
|
|
77
|
+
const supabaseUnreadErrorRule = {
|
|
78
|
+
meta: {
|
|
79
|
+
type: 'problem',
|
|
80
|
+
docs: {
|
|
81
|
+
description:
|
|
82
|
+
'Require the `error` half of a Supabase `{ data, error }` result to be read.',
|
|
83
|
+
},
|
|
84
|
+
schema: [],
|
|
85
|
+
messages: {
|
|
86
|
+
unreadError:
|
|
87
|
+
'The Supabase client never throws; it returns `{ data, error }`. Read `error` here ' +
|
|
88
|
+
'(or chain `.throwOnError()`). An unread error is a silent failure.',
|
|
89
|
+
discardedResult:
|
|
90
|
+
'The result of this Supabase call is discarded, so a failed write goes unnoticed. ' +
|
|
91
|
+
'Read `error` (or chain `.throwOnError()`).',
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
create(context) {
|
|
95
|
+
const QUERY_ROOTS = new Set(['from', 'rpc'])
|
|
96
|
+
|
|
97
|
+
/** Walks a call chain and reports whether it starts a Supabase query and whether it throws on error. */
|
|
98
|
+
function describeChain(node) {
|
|
99
|
+
let startsQuery = false
|
|
100
|
+
let throwsOnError = false
|
|
101
|
+
let current = node
|
|
102
|
+
while (current) {
|
|
103
|
+
if (current.type === 'CallExpression') {
|
|
104
|
+
const callee = current.callee
|
|
105
|
+
if (callee.type === 'MemberExpression' && callee.property.type === 'Identifier') {
|
|
106
|
+
if (QUERY_ROOTS.has(callee.property.name)) startsQuery = true
|
|
107
|
+
if (callee.property.name === 'throwOnError') throwsOnError = true
|
|
108
|
+
}
|
|
109
|
+
current = callee
|
|
110
|
+
} else if (current.type === 'MemberExpression') {
|
|
111
|
+
current = current.object
|
|
112
|
+
} else {
|
|
113
|
+
break
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return { startsQuery, throwsOnError }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function patternReadsError(pattern) {
|
|
120
|
+
return pattern.properties.some(
|
|
121
|
+
(property) =>
|
|
122
|
+
property.type === 'RestElement' ||
|
|
123
|
+
(property.key && property.key.type === 'Identifier' && property.key.name === 'error'),
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
AwaitExpression(node) {
|
|
129
|
+
const { startsQuery, throwsOnError } = describeChain(node.argument)
|
|
130
|
+
if (!startsQuery || throwsOnError) return
|
|
131
|
+
|
|
132
|
+
const parent = node.parent
|
|
133
|
+
if (parent.type === 'ExpressionStatement') {
|
|
134
|
+
context.report({ node, messageId: 'discardedResult' })
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
if (parent.type === 'VariableDeclarator' && parent.id.type === 'ObjectPattern') {
|
|
138
|
+
if (!patternReadsError(parent.id)) {
|
|
139
|
+
context.report({ node, messageId: 'unreadError' })
|
|
140
|
+
}
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
if (
|
|
144
|
+
parent.type === 'MemberExpression' &&
|
|
145
|
+
parent.object === node &&
|
|
146
|
+
parent.property.type === 'Identifier' &&
|
|
147
|
+
parent.property.name === 'data'
|
|
148
|
+
) {
|
|
149
|
+
context.report({ node, messageId: 'unreadError' })
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export const hstackPlugin = {
|
|
157
|
+
meta: { name: 'hstack', version: '1.0.0' },
|
|
158
|
+
rules: {
|
|
159
|
+
'supabase-unread-error': supabaseUnreadErrorRule,
|
|
160
|
+
},
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* @param {object} [options]
|
|
165
|
+
* @param {string[]} [options.files] Files the rules apply to.
|
|
166
|
+
* @param {string[]} [options.testFiles] Files where size rules are relaxed.
|
|
167
|
+
* @param {string[]} [options.scriptFiles] Operator CLIs, where stdout is the interface.
|
|
168
|
+
* @param {boolean} [options.typescript] Emit rules that need `@typescript-eslint` registered. Default true.
|
|
169
|
+
* @param {boolean} [options.imports] Emit rules that need `eslint-plugin-import` registered. Default false.
|
|
170
|
+
* @param {boolean} [options.supabase] Emit the unread-`error` rule. Default false.
|
|
171
|
+
* @param {Partial<typeof DEFAULT_THRESHOLDS>} [options.thresholds]
|
|
172
|
+
* @param {object} [options.modules] Where the one sanctioned path for each concern lives; used in messages.
|
|
173
|
+
* @param {string} [options.modules.logger] e.g. 'lib/observability/logger'
|
|
174
|
+
* @param {string} [options.modules.config] e.g. 'lib/env'
|
|
175
|
+
* @param {string} [options.modules.dbClient] e.g. 'lib/supabase'
|
|
176
|
+
* @returns {object[]} Flat-config objects to spread into `defineConfig([...])`.
|
|
177
|
+
*/
|
|
178
|
+
export function cleanCode(options = {}) {
|
|
179
|
+
const files = options.files ?? DEFAULT_FILES
|
|
180
|
+
const testFiles = options.testFiles ?? DEFAULT_TEST_FILES
|
|
181
|
+
const scriptFiles = options.scriptFiles ?? DEFAULT_SCRIPT_FILES
|
|
182
|
+
const typescript = options.typescript ?? true
|
|
183
|
+
const imports = options.imports ?? false
|
|
184
|
+
const supabase = options.supabase ?? false
|
|
185
|
+
const t = { ...DEFAULT_THRESHOLDS, ...(options.thresholds ?? {}) }
|
|
186
|
+
const modules = options.modules ?? {}
|
|
187
|
+
|
|
188
|
+
const loggerHint = modules.logger ? ` Use ${modules.logger}.` : ' Use the repo logger.'
|
|
189
|
+
const configHint = modules.config
|
|
190
|
+
? ` Read it through ${modules.config}, which validates every variable once at boot.`
|
|
191
|
+
: ' Read it through the one config module that validates every variable at boot.'
|
|
192
|
+
const dbClientHint = modules.dbClient
|
|
193
|
+
? ` Get a client from ${modules.dbClient}; a client built elsewhere can bypass RLS.`
|
|
194
|
+
: ' Get a client from the one sanctioned module; a client built elsewhere can bypass RLS.'
|
|
195
|
+
|
|
196
|
+
const sizeAndShape = {
|
|
197
|
+
'max-lines-per-function': [
|
|
198
|
+
'error',
|
|
199
|
+
{ max: t.functionLines, skipBlankLines: true, skipComments: true, IIFEs: true },
|
|
200
|
+
],
|
|
201
|
+
'max-lines': ['error', { max: t.fileLines, skipBlankLines: true, skipComments: true }],
|
|
202
|
+
complexity: ['error', t.complexity],
|
|
203
|
+
'max-depth': ['error', t.depth],
|
|
204
|
+
'max-params': ['error', t.params],
|
|
205
|
+
'max-nested-callbacks': ['error', t.callbacks],
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const errorsAreHandled = {
|
|
209
|
+
'no-empty': ['error', { allowEmptyCatch: false }],
|
|
210
|
+
'prefer-promise-reject-errors': 'error',
|
|
211
|
+
'no-throw-literal': 'error',
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// `no-console` carries no custom message; when the consumer names a logger,
|
|
215
|
+
// the restricted-global form says where to go instead.
|
|
216
|
+
const consoleRules = modules.logger
|
|
217
|
+
? {
|
|
218
|
+
'no-console': 'off',
|
|
219
|
+
'no-restricted-globals': [
|
|
220
|
+
'error',
|
|
221
|
+
{ name: 'console', message: `Do not log through console.${loggerHint}` },
|
|
222
|
+
],
|
|
223
|
+
}
|
|
224
|
+
: { 'no-console': ['error', { allow: [] }] }
|
|
225
|
+
const consoleOff = { 'no-console': 'off', 'no-restricted-globals': 'off' }
|
|
226
|
+
|
|
227
|
+
const onePathPerConcern = {
|
|
228
|
+
...consoleRules,
|
|
229
|
+
'no-restricted-syntax': [
|
|
230
|
+
'error',
|
|
231
|
+
{
|
|
232
|
+
selector: "MemberExpression[object.name='process'][property.name='env']",
|
|
233
|
+
message: `Do not read process.env here.${configHint}`,
|
|
234
|
+
},
|
|
235
|
+
],
|
|
236
|
+
'no-restricted-imports': [
|
|
237
|
+
'error',
|
|
238
|
+
{
|
|
239
|
+
paths: [
|
|
240
|
+
{
|
|
241
|
+
name: '@supabase/supabase-js',
|
|
242
|
+
message: `Do not import @supabase/supabase-js directly.${dbClientHint}`,
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
},
|
|
246
|
+
],
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const deadCode = {
|
|
250
|
+
// The core rule is replaced by its type-aware twin when TypeScript is on.
|
|
251
|
+
'no-unused-vars': typescript ? 'off' : 'error',
|
|
252
|
+
'no-duplicate-imports': 'error',
|
|
253
|
+
'no-unreachable': 'error',
|
|
254
|
+
'no-useless-return': 'error',
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const typescriptRules = typescript
|
|
258
|
+
? {
|
|
259
|
+
'@typescript-eslint/no-unused-vars': [
|
|
260
|
+
'error',
|
|
261
|
+
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' },
|
|
262
|
+
],
|
|
263
|
+
'@typescript-eslint/no-explicit-any': 'error',
|
|
264
|
+
'@typescript-eslint/no-non-null-assertion': 'error',
|
|
265
|
+
'@typescript-eslint/consistent-type-imports': [
|
|
266
|
+
'error',
|
|
267
|
+
{ prefer: 'type-imports', fixStyle: 'inline-type-imports' },
|
|
268
|
+
],
|
|
269
|
+
// Type-aware: needs `parserOptions.projectService` (or `project`) set by the consumer.
|
|
270
|
+
'@typescript-eslint/no-floating-promises': 'error',
|
|
271
|
+
'@typescript-eslint/only-throw-error': 'error',
|
|
272
|
+
}
|
|
273
|
+
: {}
|
|
274
|
+
|
|
275
|
+
const importRules = imports ? { 'import/no-cycle': ['error', { maxDepth: Infinity }] } : {}
|
|
276
|
+
|
|
277
|
+
const supabaseRules = supabase ? { 'hstack/supabase-unread-error': 'error' } : {}
|
|
278
|
+
|
|
279
|
+
const configs = [
|
|
280
|
+
{
|
|
281
|
+
name: 'hstack/clean-code',
|
|
282
|
+
files,
|
|
283
|
+
...(supabase ? { plugins: { hstack: hstackPlugin } } : {}),
|
|
284
|
+
rules: {
|
|
285
|
+
...sizeAndShape,
|
|
286
|
+
...errorsAreHandled,
|
|
287
|
+
...onePathPerConcern,
|
|
288
|
+
...deadCode,
|
|
289
|
+
...typescriptRules,
|
|
290
|
+
...importRules,
|
|
291
|
+
...supabaseRules,
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
// A long test is a normal test, and a test file reads the environment to
|
|
296
|
+
// decide what it can run against.
|
|
297
|
+
name: 'hstack/clean-code/tests',
|
|
298
|
+
files: testFiles,
|
|
299
|
+
rules: {
|
|
300
|
+
'max-lines-per-function': 'off',
|
|
301
|
+
'max-lines': 'off',
|
|
302
|
+
'max-nested-callbacks': 'off',
|
|
303
|
+
'no-restricted-syntax': 'off',
|
|
304
|
+
...(typescript ? { '@typescript-eslint/no-non-null-assertion': 'off' } : {}),
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
// Operator-run CLIs: stdout is the interface, and the process reads its
|
|
309
|
+
// own environment.
|
|
310
|
+
name: 'hstack/clean-code/scripts',
|
|
311
|
+
files: scriptFiles,
|
|
312
|
+
rules: {
|
|
313
|
+
...consoleOff,
|
|
314
|
+
'no-restricted-syntax': 'off',
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
]
|
|
318
|
+
|
|
319
|
+
return configs
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export default cleanCode
|
|
@@ -1,190 +1,96 @@
|
|
|
1
|
-
|
|
2
|
-
id: infrastructure
|
|
3
|
-
type: infrastructure
|
|
4
|
-
status: drafted # drafted | current | needs-refresh | archived
|
|
5
|
-
owner: <git-handle>
|
|
6
|
-
last-quarterly-review: <YYYY-MM-DD>
|
|
7
|
-
created: <YYYY-MM-DD>
|
|
8
|
-
updated: <YYYY-MM-DD>
|
|
9
|
-
schema-version: 1
|
|
10
|
-
---
|
|
1
|
+
_Where things run, why the couplings are what they are, and the traps. Operational truth, not policy — read it before touching env, deploy or dependencies (kernel § Context docs), and update it in the PR that makes it wrong. An honest "we don't have this yet" is the right answer whenever it is the true one._
|
|
11
2
|
|
|
12
|
-
|
|
3
|
+
## Where things run
|
|
13
4
|
|
|
14
|
-
|
|
5
|
+
_One row per distinct runtime — app, edge functions, workers, cron, queue consumers. Names copied from the provider console, not paraphrased._
|
|
15
6
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
| Runtime | Provider | Region(s) | Tier / Plan | Notes |
|
|
7
|
+
| Runtime | Provider | Region(s) | Plan / tier | Notes |
|
|
19
8
|
|---|---|---|---|---|
|
|
20
9
|
| | | | | |
|
|
21
10
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
_DNS provider and zone, custom domains, TLS/SSL provisioning, CDN / edge cache, load balancing, ingress rules. Note which records are managed in code (IaC) vs the provider console._
|
|
25
|
-
|
|
26
|
-
- **DNS provider.**
|
|
27
|
-
- **Domains in use.**
|
|
28
|
-
- **TLS provisioning.**
|
|
29
|
-
- **CDN / edge.**
|
|
30
|
-
- **Load balancing.**
|
|
31
|
-
- **Egress controls.**
|
|
11
|
+
- **Domains, DNS, TLS, CDN.**
|
|
12
|
+
- **Declared in code vs clicked in a console.** List the clicked resources explicitly — each one is a load-bearing piece of tribal knowledge.
|
|
32
13
|
|
|
33
|
-
## Data
|
|
14
|
+
## Data & storage
|
|
34
15
|
|
|
35
|
-
|
|
16
|
+
_Operational only. Schema, tenancy and RLS live in `data-architecture.md`._
|
|
36
17
|
|
|
37
|
-
- **Primary database
|
|
38
|
-
- **
|
|
39
|
-
- **
|
|
40
|
-
- **Point-in-time recovery.**
|
|
18
|
+
- **Primary database** — host, plan, connection pooling.
|
|
19
|
+
- **Backups** — cadence, retention, point-in-time-recovery window.
|
|
20
|
+
- **Last restore drill** — date, and what it proved. An untested backup is a wish.
|
|
41
21
|
- **Replicas / failover.**
|
|
42
|
-
- **
|
|
43
|
-
- **
|
|
44
|
-
|
|
45
|
-
## Storage
|
|
46
|
-
|
|
47
|
-
_Object storage, file uploads, presigned-URL flows, lifecycle and retention. One row per bucket / container._
|
|
48
|
-
|
|
49
|
-
| Bucket | Provider | Purpose | Public? | Lifecycle | Notes |
|
|
50
|
-
|---|---|---|---|---|---|
|
|
51
|
-
| | | | | | |
|
|
52
|
-
|
|
53
|
-
## Secrets & Configuration
|
|
54
|
-
|
|
55
|
-
_Where secrets live, how they are injected at runtime, rotation cadence, who has access. **The runbook for rotation lives in `incident-runbook.md`; this section names the system, not the steps.**_
|
|
56
|
-
|
|
57
|
-
- **Secret store.**
|
|
58
|
-
- **Injection mechanism.**
|
|
59
|
-
- **Rotation cadence.**
|
|
60
|
-
- **Access control.**
|
|
61
|
-
- **Audit trail.**
|
|
62
|
-
|
|
63
|
-
## Environment Separation
|
|
64
|
-
|
|
65
|
-
_What dev, staging (if present), and production look like, and what differs between them. Data-promotion rules (does prod data ever flow downstream? if so, with what redaction?). Connection-string isolation. **Test-against-prod is a kernel-level forbidden tool; flag any violation here as tech-debt.**_
|
|
66
|
-
|
|
67
|
-
- **Dev.**
|
|
68
|
-
- **Staging.**
|
|
69
|
-
- **Production.**
|
|
70
|
-
- **Data-promotion rules.**
|
|
71
|
-
- **Cross-environment guardrails.**
|
|
72
|
-
|
|
73
|
-
## IaC Inventory
|
|
74
|
-
|
|
75
|
-
_What infrastructure is declared in code vs configured by click in a console. Path to IaC files. Tool (Terraform, Pulumi, CDK, OpenTofu, Supabase migrations, GitHub Actions YAML, Dockerfile). Who can apply. **Click-configured infra is not forbidden, but every clicked resource is a load-bearing tribal-knowledge item; list each one explicitly so it can be promoted to IaC over time.**_
|
|
76
|
-
|
|
77
|
-
| Resource | Source-of-truth | Path / location | Applier(s) | Notes |
|
|
78
|
-
|---|---|---|---|---|
|
|
79
|
-
| | | | | |
|
|
80
|
-
|
|
81
|
-
## Deploy Pipeline
|
|
82
|
-
|
|
83
|
-
_How code reaches each environment. References `ci-cd.md` for the build pipeline; this section captures the deploy half — promotion mechanics, rollout strategy (instant cutover, canary, blue/green), rollback procedure, who can trigger a deploy._
|
|
84
|
-
|
|
85
|
-
- **Build → deploy handoff.**
|
|
86
|
-
- **Promotion path.**
|
|
87
|
-
- **Rollout strategy.**
|
|
88
|
-
- **Rollback procedure.**
|
|
89
|
-
- **Deploy authorization.**
|
|
90
|
-
|
|
91
|
-
## Observability
|
|
22
|
+
- **Vector or search infrastructure.**
|
|
23
|
+
- **Buckets** — one line each: purpose, public or not, lifecycle.
|
|
92
24
|
|
|
93
|
-
|
|
25
|
+
## Environments
|
|
94
26
|
|
|
95
|
-
|
|
96
|
-
- **Metrics.**
|
|
97
|
-
- **Traces.**
|
|
98
|
-
- **Error tracking.**
|
|
99
|
-
- **Uptime monitoring.**
|
|
100
|
-
- **Alert routing.**
|
|
101
|
-
|
|
102
|
-
## Cost & Capacity
|
|
103
|
-
|
|
104
|
-
_Current monthly spend per service (approximate is fine). Budget alerts. Scaling triggers and ceilings. Rate-limiting posture (per-route, per-tenant). **Cost is a security and reliability concern, not just a finance concern: an unbounded scale ceiling is a billing-DoS vector.**_
|
|
27
|
+
_What dev, preview and production are, and what actually differs between them._
|
|
105
28
|
|
|
106
|
-
|
|
|
29
|
+
| Environment | App URL | Database it points at | Seed data | Notes |
|
|
107
30
|
|---|---|---|---|---|
|
|
108
31
|
| | | | | |
|
|
109
32
|
|
|
110
|
-
- **
|
|
111
|
-
- **
|
|
33
|
+
- **Source of truth for environment variables**, and how they are scoped per environment.
|
|
34
|
+
- **Does production data ever flow downstream?** If so, with what redaction.
|
|
35
|
+
- **Cross-environment guardrails.** Testing against production is a kernel-forbidden path; a violation found here is a tech-debt file, not a footnote.
|
|
112
36
|
|
|
113
|
-
##
|
|
114
|
-
|
|
115
|
-
_Recovery Point Objective (RPO) and Recovery Time Objective (RTO) targets per critical resource. Backup restore procedure (high level — full steps live in `incident-runbook.md`). Drill cadence — when was the last successful restore test, and when is the next scheduled. **An untested backup is a wish, not a recovery plan.**_
|
|
116
|
-
|
|
117
|
-
| Resource | RPO target | RTO target | Last drill | Next drill |
|
|
118
|
-
|---|---|---|---|---|
|
|
119
|
-
| | | | | |
|
|
37
|
+
## Secrets
|
|
120
38
|
|
|
121
|
-
|
|
39
|
+
- **Where they live.** The one store that is authoritative.
|
|
40
|
+
- **How they reach the runtime.**
|
|
41
|
+
- **Who has access**, and how that is reviewed.
|
|
42
|
+
- **Rotation** — cadence, and where the steps are written down.
|
|
122
43
|
|
|
123
|
-
|
|
44
|
+
## Deploy Pipeline
|
|
124
45
|
|
|
125
|
-
|
|
126
|
-
|---|---|---|---|---|
|
|
127
|
-
| | | | | |
|
|
46
|
+
_How code reaches production, and the exact commands `/hstack-promote` will look for here. A command left blank halts the promotion — which is the correct outcome, because guessing a production command is how the wrong project gets migrated._
|
|
128
47
|
|
|
129
|
-
|
|
48
|
+
| What `/hstack-promote` needs | Command |
|
|
49
|
+
|---|---|
|
|
50
|
+
| Apply a pending migration to production | |
|
|
51
|
+
| List deployments and their state | |
|
|
52
|
+
| Smoke-test a specific deployment URL | |
|
|
53
|
+
| Read production logs | |
|
|
54
|
+
| Promote a deployment | |
|
|
55
|
+
| Roll back to the previous deployment | |
|
|
130
56
|
|
|
131
|
-
|
|
57
|
+
- **Promotion path.** How a merge becomes an unpromoted production build, and what promotes it. Auto-assignment of the production domain must be off for the staged flow to exist at all.
|
|
58
|
+
- **Who may promote.**
|
|
59
|
+
- **Rollback.** What re-promoting the previous deployment does and does not undo — migrations are additive, so they stay.
|
|
132
60
|
|
|
133
|
-
|
|
134
|
-
|---|---|---|---|---|
|
|
135
|
-
| | | | | |
|
|
61
|
+
## Observability
|
|
136
62
|
|
|
137
|
-
-
|
|
138
|
-
- **Audit log retention.**
|
|
139
|
-
- **Access review cadence.**
|
|
63
|
+
_Where `/hstack-promote` looks in its post-promotion window, and where a daily glance happens._
|
|
140
64
|
|
|
141
|
-
|
|
65
|
+
- **Logs** — where, retention.
|
|
66
|
+
- **Error tracking** — tool, project, who watches it.
|
|
67
|
+
- **Uptime monitoring.**
|
|
68
|
+
- **Alert routing** — which alert reaches whom, on which channel. "Nobody is paged" is an answer; write it down rather than implying one.
|
|
142
69
|
|
|
143
|
-
|
|
70
|
+
## Couplings & gotchas
|
|
144
71
|
|
|
145
|
-
|
|
146
|
-
|---|---|---|---|---|---|---|
|
|
147
|
-
| | | | | | | |
|
|
72
|
+
_The section that earns this file. Everything a newcomer — human or agent — would otherwise learn by breaking production: a region that must match another region, a version pinned because the next one broke us, a service that must be deployed before another, a quota that bites at a specific hour._
|
|
148
73
|
|
|
149
|
-
|
|
74
|
+
-
|
|
150
75
|
|
|
151
|
-
|
|
76
|
+
_External services this depends on at runtime. Criticality: `hard` = the product is down, `soft` = degraded, `optional` = no user impact._
|
|
152
77
|
|
|
153
|
-
|
|
|
78
|
+
| Provider | Purpose | Criticality | What happens when it is down |
|
|
154
79
|
|---|---|---|---|
|
|
155
80
|
| | | | |
|
|
156
81
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
- **Supabase MCP.** Dev project, staging project, production project — each on its own row. "Not wired" is an acceptable and often preferred value, especially for production.
|
|
160
|
-
- **Other MCPs** (Notion, GitHub, Linear, Figma, Slack, etc.). Each with its own access scope, token location, rotation. Notion and Slack MCPs in particular often surface external user-generated content into the session — flag them explicitly under INF-05.
|
|
161
|
-
|
|
162
|
-
## Compliance & Data Residency
|
|
163
|
-
|
|
164
|
-
_Regions where customer data lives at rest. GDPR-relevant flows (subject-access, deletion, export). Encryption at rest and in transit per data class. SOC 2 readiness gaps (this is honest enumeration, not a claim of posture). **v1 hstack does not by itself deliver SOC 2 or GDPR posture; the kernel says so explicitly. This section catalogs the gap, it does not close it.**_
|
|
82
|
+
## MCP access
|
|
165
83
|
|
|
166
|
-
-
|
|
167
|
-
- **GDPR flows.**
|
|
168
|
-
- **Encryption at rest.**
|
|
169
|
-
- **Encryption in transit.**
|
|
170
|
-
- **Known compliance gaps.**
|
|
84
|
+
_Which MCP servers are wired, where they point, what they can do. An MCP is a security boundary equivalent to the token it carries: a session with a project-scoped token has that project's full blast radius. The rules — no write-capable MCP against production outside `/hstack-promote`, and no write-capable MCP active in a session that reads user-generated tenant-scoped content — belong to the kernel § Stop conditions. This table is the inventory those rules are applied to._
|
|
171
85
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
_External SaaS the system depends on at runtime. One row per provider. Criticality reflects what happens if the provider is unavailable: `hard` = product is down, `soft` = degraded, `optional` = no user impact._
|
|
175
|
-
|
|
176
|
-
| Provider | Purpose | Criticality | Contractual SLO | Failure-mode behavior |
|
|
86
|
+
| MCP server | Points at | Access mode | Token storage | Notes |
|
|
177
87
|
|---|---|---|---|---|
|
|
178
88
|
| | | | | |
|
|
179
89
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
_Open tech-debt items pointing to infrastructure shortcomings. Each entry is a link to a `hstack/tech-debt/TD-NNNN-<slug>.md` file. This section is the bridge between the operational truth captured above and the workflow's tech-debt machinery — every honest gap surfaced during the interview should land as a TD via `/hstack:tech-debt-new --origin <change-id>`._
|
|
183
|
-
|
|
184
|
-
-
|
|
90
|
+
_"Not wired" is an acceptable value, and often the preferred one for production._
|
|
185
91
|
|
|
186
|
-
##
|
|
92
|
+
## Known gaps
|
|
187
93
|
|
|
188
|
-
|
|
94
|
+
_What is missing or unknown, named rather than implied. A gap with a shape is a file under `hstack/tech-debt/`; link it here. A gap without one — a dependency whose failure mode nobody has thought through — is a line here until someone does._
|
|
189
95
|
|
|
190
96
|
-
|
|
@@ -1,18 +1,12 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
type: roadmap
|
|
4
|
-
status: drafted # drafted | current | needs-refresh | archived
|
|
5
|
-
owner: <git-handle>
|
|
6
|
-
source: local # local | rhizome — who owns the truth; when rhizome, local edits are refused and the sync is authoritative
|
|
7
|
-
reviewed-on: <YYYY-MM-DD> # planner treats the roadmap as stale beyond 90 days
|
|
8
|
-
created: <YYYY-MM-DD>
|
|
9
|
-
updated: <YYYY-MM-DD>
|
|
10
|
-
schema-version: 1
|
|
2
|
+
reviewed-on: <YYYY-MM-DD> # past 90 days, treat this roadmap as stale rather than current
|
|
11
3
|
---
|
|
12
4
|
|
|
5
|
+
_Advisory only, never a gate (kernel § Context docs). It informs one-way-door decisions and the Forecloses / Enables section of an ADR; it never decides whether a change ships._
|
|
6
|
+
|
|
13
7
|
## Now
|
|
14
8
|
|
|
15
|
-
_What we are committed to shipping.
|
|
9
|
+
_What we are committed to shipping. Each item: one product line, one architectural-implication line. Vague fortune-telling ("might need to scale") is worse than absence._
|
|
16
10
|
|
|
17
11
|
- <item> — **architectural implication:** <one line>
|
|
18
12
|
|
|
@@ -30,12 +24,12 @@ _Plausible futures. These influence one-way-door decisions ONLY — never day-to
|
|
|
30
24
|
|
|
31
25
|
## Not on the path
|
|
32
26
|
|
|
33
|
-
_Explicit renouncements, with the reason.
|
|
27
|
+
_Explicit renouncements, with the reason._
|
|
34
28
|
|
|
35
29
|
- <item> — <why not>
|
|
36
30
|
|
|
37
31
|
## Success criteria for the current horizon
|
|
38
32
|
|
|
39
|
-
_Concrete observable indicators that Now is done and Next gets promoted.
|
|
33
|
+
_Concrete observable indicators that Now is done and Next gets promoted._
|
|
40
34
|
|
|
41
35
|
-
|
|
@@ -1,20 +1,14 @@
|
|
|
1
1
|
---
|
|
2
|
-
id:
|
|
2
|
+
id: NOTION:<page-id>
|
|
3
3
|
type: story
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
job-to-be-done: <one-sentence summary; mirrors section 1>
|
|
8
|
-
success-metric: <how shipping is measured>
|
|
9
|
-
linked-change-specs: [] # required non-empty when status: in-flight
|
|
10
|
-
created: <YYYY-MM-DD>
|
|
11
|
-
updated: <YYYY-MM-DD>
|
|
12
|
-
schema-version: 1
|
|
4
|
+
persona: <persona-slug> # the persona this story is written from
|
|
5
|
+
job-to-be-done: <one sentence; mirrors section 1>
|
|
6
|
+
success-metric: <how we know shipping it worked>
|
|
13
7
|
---
|
|
14
8
|
|
|
15
9
|
## Who and Why
|
|
16
10
|
|
|
17
|
-
|
|
11
|
+
_The persona (pointer, not duplicated) and the job to be done. 1–2 sentences._
|
|
18
12
|
|
|
19
13
|
## What Shipping Looks Like
|
|
20
14
|
|
|
@@ -22,11 +16,11 @@ _What the user sees, does, feels. One paragraph, 3–5 sentences._
|
|
|
22
16
|
|
|
23
17
|
## Success Metric
|
|
24
18
|
|
|
25
|
-
|
|
19
|
+
_The `success-metric` above, with one sentence on how it is measured._
|
|
26
20
|
|
|
27
21
|
## Edge Cases the User Cares About
|
|
28
22
|
|
|
29
|
-
_Challenge
|
|
23
|
+
_Challenge: what does the user notice if this ships but is slightly broken? 2–5 bullets._
|
|
30
24
|
|
|
31
25
|
-
|
|
32
26
|
|