@gobing-ai/spur 0.2.5 → 0.2.9
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/package.json +8 -11
- package/schemas/section-matrix.schema.json +68 -0
- package/schemas/spur-config.schema.json +71 -1
- package/schemas/state-machine-workflow.schema.json +4 -0
- package/schemas/task-batch.schema.json +53 -0
- package/spur-cli/config/config.example.yaml +25 -1
- package/spur-cli/config/rules/boundary/config-loading-ownership.yaml +54 -0
- package/spur-cli/config/rules/boundary/planning-folder-hardcode.yaml +46 -0
- package/spur-cli/config/rules/recommended-pre-check.yaml +3 -2
- package/spur-cli/config/rules/structure/test-location.yaml +10 -0
- package/spur-cli/config/rules/typescript/happy-dom-teardown.yaml +43 -0
- package/spur-cli/config/rules/typescript/no-leaky-module-mocks.yaml +71 -0
- package/spur-cli/config/rules/ui/ui-import-boundary.yaml +51 -0
- package/spur-cli/config/tasks/section-matrix.yaml +190 -0
- package/spur-cli/config/tasks/templates/brainstorm.md +29 -0
- package/spur-cli/config/tasks/templates/feature-impl.md +50 -0
- package/spur-cli/config/tasks/templates/issue.md +33 -0
- package/spur-cli/config/tasks/templates/meta.md +31 -0
- package/spur-cli/config/tasks/templates/review.md +53 -0
- package/spur-cli/config/tasks/templates/standard.md +48 -0
- package/spur-cli/config/templates/bdd/checklist.md +1 -0
- package/spur-cli/config/templates/bdd/gherkin.md +8 -0
- package/spur-cli/config/templates/docs/00_ADR.md +27 -0
- package/spur-cli/config/templates/docs/01_PRD.md +37 -0
- package/spur-cli/config/templates/docs/02_ROADMAP.md +22 -0
- package/spur-cli/config/templates/docs/03_ARCHITECTURE.md +27 -0
- package/spur-cli/config/templates/docs/04_DESIGN.md +30 -0
- package/spur-cli/config/templates/docs/05_FEATURES.md +23 -0
- package/spur-cli/config/templates/docs/99_PROJECT_CONSTITUTION.md +65 -0
- package/spur-cli/config/templates/feature/default.md +41 -0
- package/spur-cli/config/templates/task/brainstorm.md +29 -0
- package/spur-cli/config/templates/task/feature-impl.md +50 -0
- package/spur-cli/config/templates/task/issue.md +33 -0
- package/spur-cli/config/templates/task/meta.md +31 -0
- package/spur-cli/config/templates/task/review.md +52 -0
- package/spur-cli/config/templates/task/standard.md +48 -0
- package/spur-cli/config/workflows/feature-dev.yaml +92 -80
- package/spur-cli/config/workflows/feature-lifecycle.yaml +100 -0
- package/spur-cli/config/workflows/planning-pipeline.yaml +166 -0
- package/spur-cli/config/workflows/task-lifecycle.yaml +128 -0
- package/spur-cli/config/workflows/task-pipeline.yaml +244 -0
- package/spur.js +69485 -52365
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gobing-ai/spur",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
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/
|
|
53
|
-
"@gobing-ai/
|
|
54
|
-
"@gobing-ai/
|
|
55
|
-
"@gobing-ai/ts-
|
|
56
|
-
"@gobing-ai/ts-
|
|
57
|
-
"@gobing-ai/ts-
|
|
58
|
-
"@gobing-ai/ts-
|
|
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
|
|
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
|
|
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,
|
|
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,23 @@ 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"
|
|
55
|
+
- "apps/web/src/modules/config.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/**"
|