spexcode 0.1.3 → 0.1.5
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spexcode",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "SpexCode — a spec-driven, self-developing dev tool. The `spex` CLI + spec server reads the .spec tree and its git history, and serves the dashboard.",
|
|
6
6
|
"license": "MIT",
|
package/spec-cli/src/lint.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { readdirSync, readFileSync, existsSync } from 'node:fs'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
|
-
import { repoRoot, stagedFiles } from './git.js'
|
|
3
|
+
import { repoRoot, stagedFiles, git } from './git.js'
|
|
4
4
|
import { loadSpecs } from './specs.js'
|
|
5
5
|
|
|
6
6
|
export type Finding = { level: 'error' | 'warn'; rule: string; spec?: string; file?: string; msg: string }
|
|
7
7
|
|
|
8
8
|
export type LintConfig = {
|
|
9
|
-
governedRoots: string[] // dirs whose source files must each be governed by a spec (coverage)
|
|
9
|
+
governedRoots: string[] // dirs whose source files must each be governed by a spec (coverage). '.' = whole project (safe: only git-TRACKED files, so node_modules/build/nested worktrees never count).
|
|
10
10
|
sourceExtensions: string[] // extensions coverage treats as source files
|
|
11
|
+
testGlobs: string[] // globs EXCLUDED from coverage — tests aren't governed product (default ['**/*.test.*']; set [] to govern tests too)
|
|
11
12
|
identifierExtensions: string[]// extensions the altitude bare-filename signal recognises (see IDENT below)
|
|
12
13
|
altitude: { lineBudget: number; charBudget: number; sizeable: number; dense: number; steps: number }
|
|
13
14
|
maxChildren: number // breadth budget: warn at >= this many direct children
|
|
@@ -17,6 +18,7 @@ export type LintConfig = {
|
|
|
17
18
|
const DEFAULT_CONFIG: LintConfig = {
|
|
18
19
|
governedRoots: ['spec-dashboard/src', 'spec-cli/src'],
|
|
19
20
|
sourceExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
|
21
|
+
testGlobs: ['**/*.test.*'],
|
|
20
22
|
identifierExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'md'],
|
|
21
23
|
altitude: { lineBudget: 50, charBudget: 4200, sizeable: 35, dense: 1.3, steps: 3 },
|
|
22
24
|
maxChildren: 8,
|
|
@@ -33,15 +35,35 @@ export function loadConfig(root: string): LintConfig {
|
|
|
33
35
|
}
|
|
34
36
|
}
|
|
35
37
|
|
|
36
|
-
|
|
38
|
+
// a minimal glob → RegExp anchored to the full repo-relative path: `**` = any dirs, `*` = within a segment.
|
|
39
|
+
function globToRe(glob: string): RegExp {
|
|
40
|
+
const body = glob.split(/(\*\*\/|\*\*|\*|\?)/).map((seg) => {
|
|
41
|
+
if (seg === '**/') return '(?:.*/)?'
|
|
42
|
+
if (seg === '**') return '.*'
|
|
43
|
+
if (seg === '*') return '[^/]*'
|
|
44
|
+
if (seg === '?') return '[^/]'
|
|
45
|
+
return seg.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
46
|
+
}).join('')
|
|
47
|
+
return new RegExp(`^${body}$`)
|
|
48
|
+
}
|
|
37
49
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
50
|
+
// coverage enumerates source via GIT-TRACKED files (`git ls-files`, through git() which strips the hook's
|
|
51
|
+
// GIT_DIR), NOT a raw fs walk. Tracked-only auto-excludes node_modules + build output (gitignored), nested
|
|
52
|
+
// or linked worktrees + submodules (a separate index), `.git`, and anything untracked — so governedRoots
|
|
53
|
+
// '.' means "all tracked source" with no fs explosion and no hand-maintained skip list (git IS the database).
|
|
54
|
+
// Test files drop per cfg.testGlobs (default *.test.*; set [] to govern tests too).
|
|
55
|
+
function trackedSourceFiles(root: string, roots: string[], src: RegExp, testGlobs: string[]): string[] {
|
|
56
|
+
const testRes = testGlobs.map(globToRe)
|
|
57
|
+
const out = new Set<string>()
|
|
58
|
+
for (const r of roots) {
|
|
59
|
+
let listed = ''
|
|
60
|
+
try { listed = git(['-C', root, 'ls-files', '-z', '--', r]) } catch { continue }
|
|
61
|
+
for (const f of listed.split('\0')) {
|
|
62
|
+
if (!f || !src.test(f) || testRes.some((re) => re.test(f))) continue
|
|
63
|
+
out.add(f)
|
|
64
|
+
}
|
|
44
65
|
}
|
|
66
|
+
return [...out]
|
|
45
67
|
}
|
|
46
68
|
|
|
47
69
|
// code-identifier signals: camelCase | snake_case | foo( | `backticked` | /a/path.ext | bare file.ext. Only
|
|
@@ -136,8 +158,7 @@ export async function specLint(): Promise<Finding[]> {
|
|
|
136
158
|
}
|
|
137
159
|
|
|
138
160
|
// coverage: every governed source file must be claimed by at least one spec.
|
|
139
|
-
const governed
|
|
140
|
-
for (const r of cfg.governedRoots) sourceFiles(root, r, governed, srcRe)
|
|
161
|
+
const governed = trackedSourceFiles(root, cfg.governedRoots, srcRe, cfg.testGlobs)
|
|
141
162
|
// no governed source found at all → the defaults name this repo's own dirs, so an adopter who never set
|
|
142
163
|
// lint.governedRoots would otherwise see a falsely-clean board. Make it loud and point at the knob.
|
|
143
164
|
if (governed.length === 0)
|
|
@@ -77,7 +77,11 @@ export function materialize(proj = process.cwd()): string {
|
|
|
77
77
|
// only ignore paths that live INSIDE proj. The codex hooks shim now materializes at the MAIN checkout (codex
|
|
78
78
|
// reads a linked worktree's hooks from the root checkout — see harness.ts); from a linked worktree that path
|
|
79
79
|
// escapes proj (`../…`) and is gitignored by the main checkout's OWN materialize, not the worktree's.
|
|
80
|
-
|
|
80
|
+
// spexcode.local.json — the machine-local config overlay (host-specific values, e.g. an absolute worker
|
|
81
|
+
// launcher path; see portable-layout) — joins the SAME block on the same rationale: machine-specific, must
|
|
82
|
+
// never be committed. Without it an adopter who follows our own guidance to put a host path there would
|
|
83
|
+
// `git add -A` and leak it — the exact thing the overlay exists to prevent.
|
|
84
|
+
const ignorable = [...shimPaths.filter((p) => !p.startsWith('..')), 'spexcode.local.json']
|
|
81
85
|
if (ignorable.length) writeManagedBlock(join(proj, '.gitignore'), ignorable.sort().join('\n'), ['# ', ''])
|
|
82
86
|
// (5) stamp the content-hash marker LAST (so a crash mid-render leaves it stale → re-renders next gate).
|
|
83
87
|
const h = contentHash(proj)
|