@gobing-ai/spur 0.2.5 → 0.2.8

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 (42) hide show
  1. package/package.json +8 -11
  2. package/schemas/section-matrix.schema.json +68 -0
  3. package/schemas/spur-config.schema.json +71 -1
  4. package/schemas/state-machine-workflow.schema.json +4 -0
  5. package/schemas/task-batch.schema.json +53 -0
  6. package/spur-cli/config/config.example.yaml +25 -1
  7. package/spur-cli/config/rules/boundary/config-loading-ownership.yaml +54 -0
  8. package/spur-cli/config/rules/boundary/planning-folder-hardcode.yaml +46 -0
  9. package/spur-cli/config/rules/recommended-pre-check.yaml +3 -2
  10. package/spur-cli/config/rules/structure/test-location.yaml +9 -0
  11. package/spur-cli/config/rules/typescript/happy-dom-teardown.yaml +43 -0
  12. package/spur-cli/config/rules/typescript/no-leaky-module-mocks.yaml +71 -0
  13. package/spur-cli/config/rules/ui/ui-import-boundary.yaml +51 -0
  14. package/spur-cli/config/tasks/section-matrix.yaml +188 -0
  15. package/spur-cli/config/tasks/templates/brainstorm.md +29 -0
  16. package/spur-cli/config/tasks/templates/feature-impl.md +50 -0
  17. package/spur-cli/config/tasks/templates/issue.md +33 -0
  18. package/spur-cli/config/tasks/templates/meta.md +31 -0
  19. package/spur-cli/config/tasks/templates/review.md +48 -0
  20. package/spur-cli/config/tasks/templates/standard.md +48 -0
  21. package/spur-cli/config/templates/bdd/checklist.md +1 -0
  22. package/spur-cli/config/templates/bdd/gherkin.md +8 -0
  23. package/spur-cli/config/templates/docs/00_ADR.md +27 -0
  24. package/spur-cli/config/templates/docs/01_PRD.md +37 -0
  25. package/spur-cli/config/templates/docs/02_ROADMAP.md +22 -0
  26. package/spur-cli/config/templates/docs/03_ARCHITECTURE.md +27 -0
  27. package/spur-cli/config/templates/docs/04_DESIGN.md +30 -0
  28. package/spur-cli/config/templates/docs/05_FEATURES.md +23 -0
  29. package/spur-cli/config/templates/docs/99_PROJECT_CONSTITUTION.md +65 -0
  30. package/spur-cli/config/templates/feature/default.md +41 -0
  31. package/spur-cli/config/templates/task/brainstorm.md +29 -0
  32. package/spur-cli/config/templates/task/feature-impl.md +50 -0
  33. package/spur-cli/config/templates/task/issue.md +33 -0
  34. package/spur-cli/config/templates/task/meta.md +31 -0
  35. package/spur-cli/config/templates/task/review.md +52 -0
  36. package/spur-cli/config/templates/task/standard.md +48 -0
  37. package/spur-cli/config/workflows/feature-dev.yaml +88 -80
  38. package/spur-cli/config/workflows/feature-lifecycle.yaml +100 -0
  39. package/spur-cli/config/workflows/planning-pipeline.yaml +166 -0
  40. package/spur-cli/config/workflows/task-lifecycle.yaml +128 -0
  41. package/spur-cli/config/workflows/task-pipeline.yaml +238 -0
  42. package/spur.js +69295 -52352
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/spur",
3
- "version": "0.2.5",
3
+ "version": "0.2.8",
4
4
  "description": "Spur CLI — local-first harness for mainstream coding agents: constraint checking, workflow orchestration, agent health, and history analytics. Bun-native; exposes the `spur` command.",
5
5
  "keywords": [
6
6
  "spur",
@@ -49,16 +49,13 @@
49
49
  },
50
50
  "devDependencies": {
51
51
  "@commander-js/extra-typings": "^14.0.0",
52
- "@gobing-ai/spur-app": "^0.1.0",
53
- "@gobing-ai/spur-config": "^0.1.0",
54
- "@gobing-ai/spur-domain": "^0.1.0",
55
- "@gobing-ai/ts-ai-runner": "^0.3.11",
56
- "@gobing-ai/ts-dual-workflow-engine": "^0.3.11",
57
- "@gobing-ai/ts-infra": "^0.3.11",
58
- "@gobing-ai/ts-llm-jsonl-importer": "^0.3.11",
59
- "@gobing-ai/ts-rule-engine": "^0.3.11",
60
- "@gobing-ai/ts-runtime": "^0.3.11",
61
- "@gobing-ai/ts-utils": "^0.3.11",
52
+ "@gobing-ai/ts-ai-runner": "^0.4.2",
53
+ "@gobing-ai/ts-dual-workflow-engine": "^0.4.2",
54
+ "@gobing-ai/ts-infra": "^0.4.2",
55
+ "@gobing-ai/ts-llm-jsonl-importer": "^0.4.2",
56
+ "@gobing-ai/ts-rule-engine": "^0.4.2",
57
+ "@gobing-ai/ts-runtime": "^0.4.2",
58
+ "@gobing-ai/ts-utils": "^0.4.2",
62
59
  "@types/bun": "1.3.14",
63
60
  "@types/figlet": "^1.7.0",
64
61
  "@types/node-notifier": "8.0.5",
@@ -0,0 +1,68 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://gobing.ai/spur/schemas/section-matrix.schema.json",
4
+ "title": "Section-Status-Matrix Schema",
5
+ "description": "Validates config/tasks/section-matrix.yaml — which sections are required/optional/forbidden per task status. Design §3.2. Authored for ts-runtime's loadStructuredConfig validator (supports properties/required/additionalProperties + $defs/$ref/enum; NOT patternProperties/uniqueItems).",
6
+ "type": "object",
7
+ "required": ["variants"],
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "$schema": {
11
+ "type": "string",
12
+ "description": "JSON Schema ref this file validates against (loadStructuredConfig reads it)."
13
+ },
14
+ "variants": {
15
+ "type": "object",
16
+ "description": "Map of template variant → per-status matrix. Variant names are extensible (user-defined variants allowed); each value is a variant.",
17
+ "additionalProperties": { "$ref": "#/$defs/variant" }
18
+ }
19
+ },
20
+ "$defs": {
21
+ "section": {
22
+ "enum": [
23
+ "Background",
24
+ "Requirements",
25
+ "Acceptance Criteria",
26
+ "Q&A",
27
+ "Design",
28
+ "Plan",
29
+ "Solution",
30
+ "Root Cause",
31
+ "Testing",
32
+ "Review",
33
+ "References",
34
+ "Notes",
35
+ "History"
36
+ ],
37
+ "description": "Canonical task section vocabulary (DD-08). Any other value is a typo."
38
+ },
39
+ "sectionList": {
40
+ "type": "array",
41
+ "items": { "$ref": "#/$defs/section" }
42
+ },
43
+ "entry": {
44
+ "type": "object",
45
+ "additionalProperties": false,
46
+ "properties": {
47
+ "required": { "$ref": "#/$defs/sectionList" },
48
+ "optional": { "$ref": "#/$defs/sectionList" },
49
+ "forbidden": { "$ref": "#/$defs/sectionList" },
50
+ "gate": { "type": "boolean" }
51
+ }
52
+ },
53
+ "variant": {
54
+ "type": "object",
55
+ "additionalProperties": false,
56
+ "description": "A variant may declare any subset of the 7 canonical statuses (enumerated below); an unknown status key is rejected.",
57
+ "properties": {
58
+ "backlog": { "$ref": "#/$defs/entry" },
59
+ "todo": { "$ref": "#/$defs/entry" },
60
+ "wip": { "$ref": "#/$defs/entry" },
61
+ "testing": { "$ref": "#/$defs/entry" },
62
+ "blocked": { "$ref": "#/$defs/entry" },
63
+ "done": { "$ref": "#/$defs/entry" },
64
+ "cancelled": { "$ref": "#/$defs/entry" }
65
+ }
66
+ }
67
+ }
68
+ }
@@ -103,7 +103,39 @@
103
103
  "properties": {
104
104
  "default": {
105
105
  "type": "string",
106
- "description": "Default coding agent (e.g. pi, claude, codex)."
106
+ "description": "Default executor selector for `--agent auto` (resolved as an executor name first, then a legacy agent name e.g. pi, claude, codex)."
107
+ },
108
+ "executors": {
109
+ "type": "array",
110
+ "description": "Named executor profiles: each pairs a canonical agent with an optional model override. Referenced by name from `default` and `default-by-phase`.",
111
+ "items": {
112
+ "type": "object",
113
+ "required": ["name", "agent"],
114
+ "properties": {
115
+ "name": {
116
+ "type": "string",
117
+ "minLength": 1,
118
+ "description": "Executor selector name (unique across the list)."
119
+ },
120
+ "agent": {
121
+ "type": "string",
122
+ "minLength": 1,
123
+ "description": "Canonical coding agent (claude, codex, gemini, pi, opencode, antigravity-cli, openclaw, hermes, omp)."
124
+ },
125
+ "model": {
126
+ "type": "string",
127
+ "minLength": 1,
128
+ "description": "Opaque model override passed straight to the agent (e.g. zai//glm-5.2). Applied only when the user passes no explicit --model."
129
+ }
130
+ }
131
+ }
132
+ },
133
+ "default-by-phase": {
134
+ "type": "object",
135
+ "description": "Map of dev phase (e.g. dev-run, dev-review) to executor selector. A configured phase mapping is authoritative — a broken mapping fails fast and does NOT fall back to `default`.",
136
+ "additionalProperties": {
137
+ "type": "string"
138
+ }
107
139
  }
108
140
  }
109
141
  },
@@ -135,6 +167,44 @@
135
167
  "description": "Enable sensitive-data redaction in CLI output."
136
168
  }
137
169
  }
170
+ },
171
+ "tasks": {
172
+ "type": "object",
173
+ "description": "Task-folder registration (design §9). Absorbs the legacy docs/.tasks/config.json folders + base_counter concepts. Mirrors @gobing-ai/spur-config tasksConfigSchema (Zod is SSOT).",
174
+ "properties": {
175
+ "folders": {
176
+ "type": "object",
177
+ "description": "Map of task-folder path → folder config.",
178
+ "additionalProperties": {
179
+ "type": "object",
180
+ "properties": {
181
+ "baseCounter": {
182
+ "type": "integer",
183
+ "minimum": 0,
184
+ "description": "WBS base counter for new tasks in this folder. Default 0."
185
+ },
186
+ "label": {
187
+ "type": "string",
188
+ "description": "Human-readable folder label."
189
+ }
190
+ }
191
+ }
192
+ },
193
+ "active": {
194
+ "type": "string",
195
+ "description": "Default folder for `spur task create`. Default: docs/tasks."
196
+ }
197
+ }
198
+ },
199
+ "features": {
200
+ "type": "object",
201
+ "description": "Feature directory location (design §9). Mirrors @gobing-ai/spur-config featuresConfigSchema (Zod is SSOT).",
202
+ "properties": {
203
+ "dir": {
204
+ "type": "string",
205
+ "description": "Feature files directory. Default: docs/features."
206
+ }
207
+ }
138
208
  }
139
209
  }
140
210
  }
@@ -35,6 +35,10 @@
35
35
  "properties": {
36
36
  "id": { "type": "string", "minLength": 1 },
37
37
  "description": { "type": "string" },
38
+ "pause": {
39
+ "type": "boolean",
40
+ "description": "When true, the engine pauses the run at this state for HITL (E3); resume with `spur workflow continue`."
41
+ },
38
42
  "onEnter": { "type": "array", "items": { "$ref": "#/$defs/action" } },
39
43
  "onExit": { "type": "array", "items": { "$ref": "#/$defs/action" } }
40
44
  }
@@ -0,0 +1,53 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/gobing-ai/spur/schemas/task-batch.schema.json",
4
+ "title": "Task Batch Create Input",
5
+ "description": "Editor/CI aid for spur task batch-create --file input — the contract gating LLM decomposition output before any write. Every item is validated; any failure means nothing is written. RUNTIME SOURCE OF TRUTH is the Zod taskBatchSchema in @gobing-ai/spur-domain (packages/domain/src/planning/schema.ts); keep this file in sync with it.",
6
+ "type": "array",
7
+ "minItems": 1,
8
+ "items": {
9
+ "type": "object",
10
+ "required": ["name"],
11
+ "properties": {
12
+ "name": {
13
+ "type": "string",
14
+ "minLength": 1,
15
+ "description": "Task title — used in slug and heading."
16
+ },
17
+ "background": {
18
+ "type": "string",
19
+ "description": "Context and motivation for the task."
20
+ },
21
+ "requirements": {
22
+ "type": "string",
23
+ "description": "R-numbered requirement items (R1. R2. …) — a referenceable list."
24
+ },
25
+ "feature_id": {
26
+ "type": "string",
27
+ "pattern": "^[A-Z][1-9]*$",
28
+ "description": "Feature ID for traceability and Goal→Background derivation (DD-14)."
29
+ },
30
+ "parent_wbs": {
31
+ "type": "string",
32
+ "pattern": "^\\d{4}$",
33
+ "description": "Parent WBS for sub-task grouping (the single sub-task convention, X02)."
34
+ },
35
+ "priority": {
36
+ "type": "string",
37
+ "enum": ["P0", "P1", "P2", "P3"],
38
+ "description": "Task priority aligned with the feature priority scale."
39
+ },
40
+ "tags": {
41
+ "type": "array",
42
+ "items": { "type": "string" },
43
+ "description": "Free-form filtering tags."
44
+ },
45
+ "template": {
46
+ "type": "string",
47
+ "enum": ["feature-impl", "issue", "review", "meta"],
48
+ "description": "Template variant for section structure (default: the standard template)."
49
+ }
50
+ },
51
+ "additionalProperties": false
52
+ }
53
+ }
@@ -28,9 +28,33 @@ bootstrap:
28
28
  scheduler:
29
29
  enabled: false # CLI is run-once; no scheduler
30
30
 
31
+ # ── Server runtime (env-backed: PORT / HOST override; consumed by spur serve) ──
32
+ server:
33
+ port: 3000 # env: PORT
34
+ host: localhost # env: HOST
35
+ openBrowser: true # spur serve only; skip with --no-open
36
+ webDistPath: null # S5 local static asset path
31
37
  # ── Spur app section (validated by a local zod schema — UNCHANGED keys) ──
32
38
  agent:
33
- default: pi
39
+ # `default` is the executor selector for `--agent auto` (resolved as an executor
40
+ # name first, then a legacy agent name). Keep it simple, or define richer profiles:
41
+ default: omp
42
+ # Named executor profiles: pair a canonical agent with an optional opaque model
43
+ # override. Referenced by name from `default` and `default-by-phase`.
44
+ # executors:
45
+ # - name: omp
46
+ # agent: omp
47
+ # - name: omp-zai
48
+ # agent: omp
49
+ # model: zai//glm-5.2
50
+ # - name: claude
51
+ # agent: claude
52
+ # Per-phase auto routing — a MAP of dev phase → executor selector. A configured
53
+ # phase mapping is authoritative: a broken mapping fails fast (unknown executor →
54
+ # exit 2; unusable agent → exit 1) and does NOT fall back to `default`.
55
+ # default-by-phase:
56
+ # dev-run: omp-zai
57
+ # dev-review: claude
34
58
 
35
59
  rules:
36
60
  paths:
@@ -0,0 +1,54 @@
1
+ $schema: "@gobing-ai/spur/schemas/rule-file.schema.json"
2
+ # Config-loading ownership boundary — `.spur/config.yaml` has exactly ONE loader:
3
+ # `loadSpurConfig` (+ helpers) in @gobing-ai/spur-config (ADR-027). Every other surface
4
+ # consumes the typed, validated result through that facade; none parses or schema-validates
5
+ # the config itself. Before ADR-027 there were five parallel loaders (CLI structured-config,
6
+ # app raw-yaml, CLI resolveConfigFile, server inline literals, server JSONC read) — the drift
7
+ # behind the phase-folder bugs. These rules block that class from returning.
8
+ #
9
+ # NOTE: a bare `config.yaml` string is NOT banned — `CLI_CONFIG.configFile`, the init seeder,
10
+ # and existence checks legitimately name the path. The anti-pattern is *parsing config into a
11
+ # typed shape outside the facade*, caught precisely below (no false positives on frontmatter /
12
+ # workflow YAML parsing, which share `parseYaml` for unrelated inputs). Pattern is the ripgrep
13
+ # dialect (no lookbehind/backrefs).
14
+ rules:
15
+ - id: structured-config-loader-only-in-config
16
+ description: >
17
+ ts-runtime's `loadStructuredConfig` is the schema-validating config loader and may be
18
+ called ONLY inside packages/config — it is the single implementation behind
19
+ @gobing-ai/spur-config's `loadSpurConfig`/`loadStructuredSpurConfig`. Any other surface
20
+ must consume the typed result through that facade, never re-invoke the raw loader (that
21
+ was the CLI-vs-app divergence ADR-027 removed). (ADR-027)
22
+ severity: error
23
+ evaluator:
24
+ type: rg
25
+ config:
26
+ pattern: "loadStructuredConfig\\("
27
+ include:
28
+ - "apps/**/src/**/*.ts"
29
+ - "packages/**/src/**/*.ts"
30
+ exclude:
31
+ - "packages/config/src/**"
32
+ - "**/node_modules/**"
33
+ - "**/tests/**"
34
+
35
+ - id: no-legacy-tasks-config-jsonc
36
+ description: >
37
+ The rd3 `docs/.tasks/config.jsonc` config location is retired — task/feature folders come
38
+ from `.spur/config.yaml` via the spur-config facade. Do not read, reference, or recreate it
39
+ (the server folders endpoint regressed onto it once). Matches code-context uses, not prose
40
+ that merely names the retired path. (ADR-027)
41
+ severity: error
42
+ evaluator:
43
+ type: rg
44
+ config:
45
+ # Require a code-context prefix ( = ( , quote join readFile ) before the literal so a
46
+ # comment noting the path is retired does not trip the rule. ripgrep dialect.
47
+ pattern: "[=(,]\\s*['\"`][^'\"`]*config\\.jsonc['\"`]|['\"`][^'\"`]*\\.tasks/config['\"`]"
48
+ include:
49
+ - "apps/**/src/**/*.ts"
50
+ - "apps/**/src/**/*.tsx"
51
+ - "packages/**/src/**/*.ts"
52
+ exclude:
53
+ - "**/node_modules/**"
54
+ - "**/tests/**"
@@ -0,0 +1,46 @@
1
+ $schema: "@gobing-ai/spur/schemas/rule-file.schema.json"
2
+ # Planning-folder hardcode boundary — phase folders (rd3:tasks heritage) require
3
+ # that every task/feature folder be DERIVED from `.spur/config.yaml` (tasks.active
4
+ # / features.dir), never written as an inline `docs/tasks` / `docs/features` string.
5
+ # A hardcode makes tasks created in a non-active phase folder (e.g. docs/tasks2)
6
+ # invisible to the hardcoded surface — the class of bug behind the false-positive
7
+ # write-guard deny and the server/web/team-service folder drift.
8
+ #
9
+ # The ONLY sanctioned literal lives in @gobing-ai/spur-config as DEFAULT_TASKS_DIR /
10
+ # DEFAULT_FEATURES_DIR. Everything else derives via resolvePlanningFolders (app),
11
+ # loadTaskFoldersConfig (CLI), or the task.folders endpoint (web). Pattern is the
12
+ # ripgrep dialect (no lookbehind/backrefs).
13
+ include:
14
+ - "apps/**/src/**/*.ts"
15
+ - "apps/**/src/**/*.tsx"
16
+ - "packages/**/src/**/*.ts"
17
+ exclude:
18
+ - "**/node_modules/**"
19
+ - "**/tests/**"
20
+ # The single source of truth — the literal is allowed to exist here.
21
+ - "packages/config/src/index.ts"
22
+ # The web board's documented first-paint placeholder (BOOTSTRAP_FOLDER), replaced
23
+ # on mount by the task.folders endpoint; the browser bundle stays config-dep-free.
24
+ - "apps/web/src/modules/task-kanban/KanbanBoard.tsx"
25
+ # Cloudflare Workers path: context.ts imports DEFAULT_* from the CF-safe core entry of
26
+ # @gobing-ai/spur-config (plain string constants, no yaml/node:fs). No inline literal
27
+ # remains — the former exception narrowed to a core-only import (ADR-027).
28
+ # The folders endpoint derives its response from ctx.planningFolders(), never from a
29
+ # hardcoded path or the retired docs/.tasks/config.jsonc.
30
+ rules:
31
+ - id: no-hardcoded-planning-folder
32
+ description: >
33
+ Do not hardcode the `docs/tasks` or `docs/features` folder path as a string
34
+ literal used as a VALUE (assignment, argument, fallback, or property). Phase
35
+ folders mean the active/registered folders come from `.spur/config.yaml` —
36
+ derive them via resolvePlanningFolders (app), loadTaskFoldersConfig (CLI), or
37
+ the DEFAULT_TASKS_DIR/DEFAULT_FEATURES_DIR constants in @gobing-ai/spur-config.
38
+ A literal silently ignores phase folders and loses tasks created outside the
39
+ active one. (Matches only code-context uses, not comments mentioning the path.)
40
+ severity: error
41
+ evaluator:
42
+ type: rg
43
+ config:
44
+ # Require a code-context prefix ( = ?? ( , : ) before the literal so prose
45
+ # comments that merely name the path do not trip the rule. ripgrep dialect.
46
+ pattern: "[=(,:]\\s*['\"`]docs/(tasks|features)2?['\"`]|fs\\.resolve\\(\\s*['\"`]docs['\"`]\\s*,\\s*['\"`](tasks|features)['\"`]"
@@ -3,11 +3,12 @@ $schema: "@gobing-ai/spur/schemas/preset.schema.json"
3
3
  # Use with: spur rule run --preset recommended-pre-check
4
4
  #
5
5
  # Runs BEFORE tests (test-pre-check). TypeScript quality, structural, DB
6
- # boundary, and CLI surface consistency rules. TSDoc exports run in post-check
7
- # alongside coverage-gate.
6
+ # boundary, CLI surface consistency, and UI seam-boundary rules. TSDoc exports
7
+ # run in post-check alongside coverage-gate.
8
8
  name: recommended-pre-check
9
9
  extends:
10
10
  - typescript
11
11
  - structure
12
12
  - boundary
13
13
  - surface
14
+ - ui
@@ -33,13 +33,22 @@ rules:
33
33
  requireCorrespondingTest: true
34
34
  include:
35
35
  - "packages/**/src/**/*.ts"
36
+ - "packages/**/src/**/*.tsx"
36
37
  - "apps/**/src/**/*.ts"
38
+ - "apps/**/src/**/*.tsx"
39
+
37
40
  exclude:
38
41
  - "**/index.ts"
42
+ - "**/*.tsx"
39
43
  - "**/*.d.ts"
40
44
  - "**/types.ts"
41
45
  - "**/schema/**"
46
+ - "**/schema.ts"
42
47
  - "**/migrations.ts"
43
48
  - "**/db.ts"
49
+ ## Entity declarations — contracts are transport DTOs tested by the single contract.test.ts
50
+ - "packages/contracts/src/**"
44
51
  - "**/node_modules/**"
45
52
  - "**/tests/**"
53
+ ## pure re-export for UI components
54
+ - "apps/web/src/ui.ts"
@@ -0,0 +1,43 @@
1
+ $schema: "@gobing-ai/spur/schemas/rule-file.schema.json"
2
+ # Enforce draining React's scheduler before tearing down happy-dom in web tests.
3
+ #
4
+ # WHY: React 19's scheduler defers render work onto a macrotask (MessageChannel →
5
+ # performWorkUntilDeadline). A web test's `afterAll` that calls
6
+ # `GlobalRegistrator.unregister()` directly removes `globalThis.window` while that
7
+ # work may still be queued; the deferred callback then fires with no `window` and
8
+ # throws `ReferenceError: window is not defined`, surfacing as bun's "Unhandled
9
+ # error between tests". It is timing-dependent — a fast dev box drains the queue
10
+ # before teardown, a loaded CI runner does not — so it passes locally and fails
11
+ # only in CI. (Real incident: task-detail.test.tsx, 1 CI-only error.)
12
+ #
13
+ # FIX: route teardown through the shared `teardownHappyDom` helper
14
+ # (apps/web/tests/happy-dom.ts), which unmounts, yields macrotasks so the scheduler
15
+ # drains while `window` still exists, and only then unregisters. Use it as the
16
+ # `afterAll` body in every web React test: `afterAll(teardownHappyDom);`
17
+ #
18
+ # The helper itself is the one legitimate caller of `unregister()` and is excluded.
19
+ # Severity is `warning`: convert to the helper or justify, not an automatic block.
20
+ include:
21
+ - "apps/web/tests/**/*.test.ts"
22
+ - "apps/web/tests/**/*.test.tsx"
23
+ exclude:
24
+ - "**/node_modules/**"
25
+ - "**/dist/**"
26
+ # The shared teardown helper is the sanctioned place that calls unregister().
27
+ - "apps/web/tests/happy-dom.ts"
28
+
29
+ rules:
30
+ - id: happy-dom-teardown-via-helper
31
+ description: >
32
+ Do not call `GlobalRegistrator.unregister()` directly in a web test. React 19
33
+ schedules render work on a macrotask; unregistering happy-dom before it drains
34
+ removes `window` and throws "window is not defined" — a CI-only, timing-
35
+ dependent failure. Use `afterAll(teardownHappyDom)` from apps/web/tests/happy-dom.ts,
36
+ which drains the scheduler before tearing the DOM down.
37
+ severity: warning
38
+ evaluator:
39
+ type: rg
40
+ config:
41
+ # Match the call anywhere on a line (it usually sits inside an afterAll body),
42
+ # so both `await GlobalRegistrator.unregister()` and a bare call are caught.
43
+ pattern: "GlobalRegistrator\\.unregister\\("
@@ -0,0 +1,71 @@
1
+ $schema: "@gobing-ai/spur/schemas/rule-file.schema.json"
2
+ # Guard against process-global module-mock leakage in bun:test.
3
+ #
4
+ # WHY: Bun's `mock.module()` installs a process-wide override that
5
+ # `mock.restore()` does NOT revert (per Bun docs). A `mock.module()` in one
6
+ # test file therefore leaks into every later file in the same `bun test`
7
+ # process. Because Bun discovers test files in filesystem order (sorted on
8
+ # macOS, inode order on Linux/CI), this surfaces as tests that pass locally
9
+ # but fail only in CI — exactly the 51-failure regression in
10
+ # apps/cli/tests/commands/{command,content-command-modules}.test.ts.
11
+ #
12
+ # SCOPE: only FIRST-PARTY mocks (relative `./`/`../` or `@gobing-ai/*` specifiers)
13
+ # are flagged. A `mock.module()` only leaks harmfully when another test file
14
+ # statically imports the SAME module expecting the real implementation — which only
15
+ # happens for the project's own modules. Third-party module swaps (e.g. `mermaid`,
16
+ # `@dnd-kit/core`, `@uiw/react-md-editor`) are never asserted-real by another test,
17
+ # so they cannot cause this failure and are intentionally NOT flagged.
18
+ #
19
+ # FIX: prefer `spyOn(namespaceImport, 'export')` over `mock.module()` — spyOn IS
20
+ # reverted by `mock.restore()`/`mockRestore()` in afterEach, so it cannot leak.
21
+ # CAVEAT: spyOn does NOT work on a callable Proxy (e.g. the oRPC `api` client) —
22
+ # property access regenerates the method, bypassing the spy and hitting the network.
23
+ # For such modules `mock.module()` is the only working option; keep it file-local and
24
+ # ensure no later test statically imports the real module (dynamic `await import()`
25
+ # in the reader is immune). This rule is a `warning`, not an `error`: treat a hit as
26
+ # "justify or convert," not an automatic block.
27
+ include:
28
+ # Both .ts and .tsx — the React component tests (.tsx) are where mock.module is
29
+ # most common, and they were previously invisible to this rule (the original globs
30
+ # only matched .test.ts), letting a cross-file leak ship undetected.
31
+ - "apps/**/tests/**/*.test.ts"
32
+ - "apps/**/tests/**/*.test.tsx"
33
+ - "packages/**/tests/**/*.test.ts"
34
+ - "packages/**/tests/**/*.test.tsx"
35
+ exclude:
36
+ - "**/node_modules/**"
37
+ - "**/dist/**"
38
+ # These tests mock the FIRST-PARTY oRPC `api` client (`../../../src/lib/rpc-client`).
39
+ # That client is a callable Proxy, so spyOn cannot intercept it (the call regenerates
40
+ # and hits the network) — `mock.module()` is the only working mechanism. They are safe
41
+ # because they sort AFTER both rpc-client unit-test files (`lib/rpc-client.test.ts`,
42
+ # `rpc-client.test.ts`), so the override never leaks back into a real-import reader.
43
+ # Keep this list minimal: only files that mock a first-party module and have no
44
+ # spyOn-able alternative belong here. (markdown-body mocks only third-party modules —
45
+ # mermaid/dompurify — which the narrowed pattern no longer flags, so it is not listed.)
46
+ - "apps/web/tests/modules/task-kanban/board.test.tsx"
47
+ - "apps/web/tests/modules/task-kanban/components.test.tsx"
48
+ - "apps/web/tests/modules/task-kanban/new-task-panel.test.tsx"
49
+ - "apps/web/tests/modules/task-kanban/task-detail.test.tsx"
50
+
51
+ rules:
52
+ - id: no-leaky-mock-module
53
+ description: >
54
+ Avoid `mock.module()` of a first-party module (relative path or @gobing-ai/*)
55
+ in bun:test files — it is process-global, is not reverted by mock.restore(),
56
+ and leaks into any later test file that statically imports the same module,
57
+ causing CI-only, file-order-dependent failures. Prefer spyOn() on a namespace
58
+ import (`import * as ns; spyOn(ns, 'fn')`), restored in afterEach. If the target
59
+ is a callable Proxy (e.g. the oRPC api client) spyOn cannot intercept it; keep
60
+ the mock.module() file-local and ensure readers use dynamic `await import()`.
61
+ Third-party module swaps are not flagged (they never leak harmfully).
62
+ severity: warning
63
+ evaluator:
64
+ type: rg
65
+ config:
66
+ # First-party only: line-anchored `mock.module(` whose specifier starts with a
67
+ # relative path (`.`) or the `@gobing-ai/` scope. Third-party bare specifiers
68
+ # (mermaid, @dnd-kit/*, @uiw/*) are excluded — they cannot cause the leak. The
69
+ # line anchor keeps prose mentions in comments from false-positiving.
70
+ pattern: "^\\s*mock\\.module\\(\\s*['\"](\\.|@gobing-ai/)"
71
+ flags: "m"
@@ -0,0 +1,51 @@
1
+ $schema: "@gobing-ai/spur/schemas/rule-file.schema.json"
2
+ # UI import + class-leak boundary rules — keep `apps/web/src/ui.ts` the single UI
3
+ # import seam and keep daisyUI component classes confined to `components/ui/`.
4
+ # Both rules are `error` (promoted from warning in Task 0104) and wired into the
5
+ # recommended-pre-check preset, so regressions are blocked at the standing gate.
6
+ # Two distinct seams: `ui.ts` is the single IMPORT seam (Rule A); `components/ui/`
7
+ # is the single daisyUI-CLASS-authoring seam (Rule B) — a re-export barrel cannot
8
+ # hold a className, so Rule B alone keeps the `components/ui/**` exclude.
9
+ # Evaluators proven in boundary/dao-boundary.yaml.
10
+ rules:
11
+ - id: ui-import-seam-only
12
+ description: >
13
+ Third-party UI libraries (daisyui, @uiw/react-md-editor, future UI libs)
14
+ may only be imported through the `apps/web/src/ui.ts` seam. Everything else
15
+ in apps/web — including the wrappers under `components/ui/` — imports from
16
+ `@/ui`, never the raw library. `ui.ts` is the single UI import point.
17
+ severity: error
18
+ evaluator:
19
+ type: forbidden-import
20
+ config:
21
+ forbidden:
22
+ - specifier: "daisyui"
23
+ - specifier: "@uiw/react-md-editor"
24
+ scope:
25
+ include:
26
+ - "apps/web/src/**/*.ts"
27
+ - "apps/web/src/**/*.tsx"
28
+ exclude:
29
+ - "apps/web/src/ui.ts"
30
+ - "**/tests/**"
31
+ - "**/node_modules/**"
32
+
33
+ - id: no-daisyui-class-leak
34
+ description: >
35
+ daisyUI COMPONENT classes (btn, card, badge, modal, ...) must not appear in
36
+ className strings outside `apps/web/src/components/ui/`. Layout/utility classes
37
+ (flex, grid, gap-*, p-*) are unaffected — the pattern is anchored to a
38
+ className context and word-bounded to the component-class allowlist only.
39
+ `components/ui/` stays excluded: it is the sanctioned class-authoring site.
40
+ severity: error
41
+ evaluator:
42
+ type: rg
43
+ config:
44
+ pattern: "className=[\"'][^\"']*\\b(btn|card|badge|modal|menu|navbar|drawer|tabs|alert|dropdown|collapse|join|tooltip|loading|select|checkbox|toggle)\\b"
45
+ include:
46
+ - "apps/web/src/**/*.tsx"
47
+ exclude:
48
+ - "apps/web/src/components/ui/**"
49
+ - "apps/web/src/ui.ts"
50
+ - "**/tests/**"
51
+ - "**/node_modules/**"