spexcode 0.1.1 → 0.1.3
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/LICENSE +21 -0
- package/package.json +22 -11
- package/{bin → spec-cli/bin}/spex.mjs +6 -2
- package/spec-cli/hooks/dispatch.sh +74 -0
- package/spec-cli/hooks/harness.sh +178 -0
- package/{src → spec-cli/src}/cli.ts +14 -0
- package/{src → spec-cli/src}/harness.ts +11 -21
- package/{src → spec-cli/src}/init.ts +13 -0
- package/{src → spec-cli/src}/layout.ts +23 -25
- package/{src → spec-cli/src}/materialize.ts +2 -1
- package/spec-cli/src/relay.ts +28 -0
- package/{src → spec-cli/src}/supervise.ts +2 -1
- package/spec-cli/src/tsx-bin.ts +11 -0
- package/{dashboard-dist/assets/index-B60MILFg.js → spec-dashboard/dist/assets/index-Bk4E1EQy.js} +29 -29
- package/{dashboard-dist → spec-dashboard/dist}/index.html +1 -1
- package/spec-forge/src/__fixtures__/github-forge.json +63 -0
- package/spec-forge/src/cache.ts +28 -0
- package/spec-forge/src/cli.ts +110 -0
- package/spec-forge/src/drivers/github.ts +73 -0
- package/spec-forge/src/links.ts +76 -0
- package/spec-forge/src/needs-yatsu-eval.ts +30 -0
- package/spec-forge/src/port.ts +23 -0
- package/spec-forge/src/resident.ts +23 -0
- package/spec-yatsu/src/cache.ts +68 -0
- package/spec-yatsu/src/cli.ts +296 -0
- package/spec-yatsu/src/evaltab.ts +112 -0
- package/spec-yatsu/src/evaluator.ts +24 -0
- package/spec-yatsu/src/freshness.ts +51 -0
- package/spec-yatsu/src/proof.ts +491 -0
- package/spec-yatsu/src/sidecar.ts +44 -0
- package/spec-yatsu/src/yatsu.ts +181 -0
- /package/{src → spec-cli/src}/board.ts +0 -0
- /package/{src → spec-cli/src}/client.ts +0 -0
- /package/{src → spec-cli/src}/gateway.ts +0 -0
- /package/{src → spec-cli/src}/git.ts +0 -0
- /package/{src → spec-cli/src}/guide.ts +0 -0
- /package/{src → spec-cli/src}/hooks.ts +0 -0
- /package/{src → spec-cli/src}/index.ts +0 -0
- /package/{src → spec-cli/src}/lint.ts +0 -0
- /package/{src → spec-cli/src}/login-page.ts +0 -0
- /package/{src → spec-cli/src}/pty-bridge.ts +0 -0
- /package/{src → spec-cli/src}/ranker.ts +0 -0
- /package/{src → spec-cli/src}/resilience.ts +0 -0
- /package/{src → spec-cli/src}/search.bench.mjs +0 -0
- /package/{src → spec-cli/src}/search.ts +0 -0
- /package/{src → spec-cli/src}/self.ts +0 -0
- /package/{src → spec-cli/src}/sessions.ts +0 -0
- /package/{src → spec-cli/src}/slash-commands.ts +0 -0
- /package/{src → spec-cli/src}/specs.ts +0 -0
- /package/{src → spec-cli/src}/uploads.ts +0 -0
- /package/{templates → spec-cli/templates}/hooks/pre-commit +0 -0
- /package/{templates → spec-cli/templates}/hooks/prepare-commit-msg +0 -0
- /package/{templates → spec-cli/templates}/spec/project/.config/core/idle/idle.sh +0 -0
- /package/{templates → spec-cli/templates}/spec/project/.config/core/idle/spec.md +0 -0
- /package/{templates → spec-cli/templates}/spec/project/.config/core/mark-active/mark-active.sh +0 -0
- /package/{templates → spec-cli/templates}/spec/project/.config/core/mark-active/spec.md +0 -0
- /package/{templates → spec-cli/templates}/spec/project/.config/core/session-fail/fail.sh +0 -0
- /package/{templates → spec-cli/templates}/spec/project/.config/core/session-fail/spec.md +0 -0
- /package/{templates → spec-cli/templates}/spec/project/.config/core/spec-first/spec-first.sh +0 -0
- /package/{templates → spec-cli/templates}/spec/project/.config/core/spec-first/spec.md +0 -0
- /package/{templates → spec-cli/templates}/spec/project/.config/core/spec-of-file/spec-of-file.sh +0 -0
- /package/{templates → spec-cli/templates}/spec/project/.config/core/spec-of-file/spec.md +0 -0
- /package/{templates → spec-cli/templates}/spec/project/.config/core/spec.md +0 -0
- /package/{templates → spec-cli/templates}/spec/project/.config/core/stop-gate/spec.md +0 -0
- /package/{templates → spec-cli/templates}/spec/project/.config/core/stop-gate/stop-gate.sh +0 -0
- /package/{templates → spec-cli/templates}/spec/project/.config/extract/spec.md +0 -0
- /package/{templates → spec-cli/templates}/spec/project/.config/forge-link/spec.md +0 -0
- /package/{templates → spec-cli/templates}/spec/project/.config/memory-hygiene/spec.md +0 -0
- /package/{templates → spec-cli/templates}/spec/project/.config/regroup/spec.md +0 -0
- /package/{templates → spec-cli/templates}/spec/project/.config/scenario/spec.md +0 -0
- /package/{templates → spec-cli/templates}/spec/project/.config/spec.md +0 -0
- /package/{templates → spec-cli/templates}/spec/project/.config/supervisor/spec.md +0 -0
- /package/{templates → spec-cli/templates}/spec/project/.config/tidy/spec.md +0 -0
- /package/{templates → spec-cli/templates}/spec/project/spec.md +0 -0
- /package/{templates → spec-cli/templates}/spexcode.json +0 -0
- /package/{dashboard-dist → spec-dashboard/dist}/assets/index-Cq7hwngj.css +0 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, existsSync } from 'node:fs'
|
|
2
|
+
import { join, relative, basename } from 'node:path'
|
|
3
|
+
|
|
4
|
+
export const YATSU_FILE = 'yatsu.md'
|
|
5
|
+
export const SIDECAR_FILE = 'yatsu.evals.ndjson'
|
|
6
|
+
|
|
7
|
+
export type Scenario = {
|
|
8
|
+
name: string
|
|
9
|
+
description: string
|
|
10
|
+
expected: string
|
|
11
|
+
test?: string
|
|
12
|
+
code?: string[]
|
|
13
|
+
related?: string[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type YatsuNode = {
|
|
17
|
+
id: string // the node's leaf dir name (its spec-node id)
|
|
18
|
+
dir: string // absolute node directory
|
|
19
|
+
yatsuPath: string // repo-relative path to yatsu.md — the SCENARIO freshness axis
|
|
20
|
+
sidecarPath: string // absolute path to yatsu.evals.ndjson
|
|
21
|
+
scenarios: Scenario[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const SCENARIO_KEYS = ['name', 'description', 'expected', 'test', 'code', 'related'] as const
|
|
25
|
+
type ScenarioKey = (typeof SCENARIO_KEYS)[number]
|
|
26
|
+
const LIST_KEYS: readonly ScenarioKey[] = ['code', 'related']
|
|
27
|
+
|
|
28
|
+
// a raw scenario item straight off the frontmatter walk: the known fields it set, plus any UNKNOWN keys it
|
|
29
|
+
// carried — kept (not dropped) so the validator can name a typo'd field instead of silently swallowing it.
|
|
30
|
+
type RawItem = { fields: Partial<Record<ScenarioKey, string>>; unknownKeys: string[] }
|
|
31
|
+
|
|
32
|
+
// tiny indentation parser for yatsu.md's frontmatter `scenarios:` block (no YAML dep), shared by parseScenarios and validateScenarios so they can't disagree; reports hasFrontmatter/hasKey so the validator can tell "none declared" from "malformed"
|
|
33
|
+
function walkScenarios(src: string): { hasFrontmatter: boolean; hasKey: boolean; items: RawItem[] } {
|
|
34
|
+
const m = src.match(/^---\n([\s\S]*?)\n---/)
|
|
35
|
+
if (!m) return { hasFrontmatter: false, hasKey: false, items: [] }
|
|
36
|
+
const lines = m[1].split('\n')
|
|
37
|
+
let i = lines.findIndex((l) => /^scenarios:\s*$/.test(l))
|
|
38
|
+
if (i < 0) return { hasFrontmatter: true, hasKey: false, items: [] }
|
|
39
|
+
const items: RawItem[] = []
|
|
40
|
+
let cur: RawItem | null = null
|
|
41
|
+
let itemIndent = -1 // the indent of the `- ` that starts each scenario (set by the first one)
|
|
42
|
+
const indentOf = (l: string) => l.length - l.replace(/^\s+/, '').length
|
|
43
|
+
for (i++; i < lines.length; i++) {
|
|
44
|
+
const line = lines[i]
|
|
45
|
+
if (!line.trim()) continue
|
|
46
|
+
const indent = indentOf(line)
|
|
47
|
+
if (indent === 0) break // dedented to another top-level key — scenarios block is done
|
|
48
|
+
const trimmed = line.trim()
|
|
49
|
+
const dash = trimmed.startsWith('- ') || trimmed === '-'
|
|
50
|
+
if (dash && (itemIndent < 0 || indent <= itemIndent)) {
|
|
51
|
+
// a new scenario item. start fresh; the `- ` may carry the first field inline.
|
|
52
|
+
cur = { fields: {}, unknownKeys: [] }
|
|
53
|
+
items.push(cur)
|
|
54
|
+
itemIndent = indent
|
|
55
|
+
const inline = trimmed.slice(1).trim() // text after the dash
|
|
56
|
+
if (inline) i = assignField(cur, inline, lines, i, indent)
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
if (!cur) continue // content before the first dash — ignore
|
|
60
|
+
i = assignField(cur, trimmed, lines, i, indent)
|
|
61
|
+
}
|
|
62
|
+
return { hasFrontmatter: true, hasKey: true, items }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// assign a `key: value` field to the current item. When the value is a block-scalar indicator (`|`
|
|
66
|
+
// literal / `>` folded), consume the following more-indented lines as the value and return the index of
|
|
67
|
+
// the LAST consumed line (the for-loop's ++ then moves past it); otherwise return `idx` unchanged. A key
|
|
68
|
+
// outside the schema is recorded under unknownKeys (still consuming its block, so the body isn't misread as
|
|
69
|
+
// new items) rather than dropped — validateScenarios needs to see it to reject the typo.
|
|
70
|
+
function assignField(cur: RawItem, kv: string, lines: string[], idx: number, keyIndent: number): number {
|
|
71
|
+
const f = kv.match(/^([A-Za-z_][\w-]*):\s*(.*)$/)
|
|
72
|
+
if (!f) return idx
|
|
73
|
+
const key = f[1]
|
|
74
|
+
// a list field (`code:`/`related:`) may be a YAML block sequence (`- item` lines); the scalar reader can't see those, so collect them here into the comma form parseCodeList expects
|
|
75
|
+
if ((LIST_KEYS as readonly string[]).includes(key) && f[2].trim() === '') {
|
|
76
|
+
const items: string[] = []
|
|
77
|
+
let j = idx + 1
|
|
78
|
+
for (; j < lines.length; j++) {
|
|
79
|
+
const l = lines[j]
|
|
80
|
+
if (!l.trim()) continue
|
|
81
|
+
const ind = l.length - l.replace(/^\s+/, '').length
|
|
82
|
+
if (ind <= keyIndent) break
|
|
83
|
+
const it = l.trim().match(/^-\s*(.+)$/)
|
|
84
|
+
if (!it) break
|
|
85
|
+
items.push(unquote(it[1]))
|
|
86
|
+
}
|
|
87
|
+
if (items.length) { cur.fields[key as ScenarioKey] = items.join(','); return j - 1 }
|
|
88
|
+
}
|
|
89
|
+
let value: string
|
|
90
|
+
let end = idx
|
|
91
|
+
const block = f[2].match(/^([|>])[+-]?\s*$/)
|
|
92
|
+
if (block) {
|
|
93
|
+
const fold = block[1] === '>'
|
|
94
|
+
const body: string[] = []
|
|
95
|
+
let base = -1, j = idx + 1
|
|
96
|
+
for (; j < lines.length; j++) {
|
|
97
|
+
const l = lines[j]
|
|
98
|
+
if (!l.trim()) { body.push(''); continue }
|
|
99
|
+
const ind = l.length - l.replace(/^\s+/, '').length
|
|
100
|
+
if (ind <= keyIndent) break // dedented to a sibling field / next item → the block is done
|
|
101
|
+
if (base < 0) base = ind
|
|
102
|
+
body.push(l.slice(base))
|
|
103
|
+
}
|
|
104
|
+
while (body.length && body[body.length - 1] === '') body.pop() // strip trailing blanks
|
|
105
|
+
value = fold ? body.join(' ').replace(/\s+/g, ' ').trim() : body.join('\n')
|
|
106
|
+
end = j - 1
|
|
107
|
+
} else {
|
|
108
|
+
value = unquote(f[2])
|
|
109
|
+
}
|
|
110
|
+
if ((SCENARIO_KEYS as readonly string[]).includes(key)) cur.fields[key as ScenarioKey] = value
|
|
111
|
+
else cur.unknownKeys.push(key)
|
|
112
|
+
return end
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const unquote = (s: string) => s.replace(/^["'](.*)["']$/, '$1').trim()
|
|
116
|
+
|
|
117
|
+
// a scenario's optional list field (`code:`/`related:`) is a comma-separated path list (a YAML flow list
|
|
118
|
+
// `[a, b]` or bare `a, b`, or a single path) — the tiny parser stays scalar-only, so it is split here.
|
|
119
|
+
function parseCodeList(raw: string): string[] {
|
|
120
|
+
return raw.replace(/^\[|\]$/g, '').split(',').map((s) => unquote(s.trim())).filter(Boolean)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function parseScenarios(src: string): Scenario[] {
|
|
124
|
+
return walkScenarios(src).items
|
|
125
|
+
.map((it): Scenario => {
|
|
126
|
+
const code = it.fields.code ? parseCodeList(it.fields.code) : []
|
|
127
|
+
const related = it.fields.related ? parseCodeList(it.fields.related) : []
|
|
128
|
+
return {
|
|
129
|
+
name: it.fields.name ?? '',
|
|
130
|
+
description: it.fields.description ?? '',
|
|
131
|
+
expected: it.fields.expected ?? '',
|
|
132
|
+
...(it.fields.test ? { test: it.fields.test } : {}),
|
|
133
|
+
...(code.length ? { code } : {}),
|
|
134
|
+
...(related.length ? { related } : {}),
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
.filter((s) => s.name) // a scenario with no name is malformed — drop it (validateScenarios reports it)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function validateScenarios(src: string): string[] {
|
|
141
|
+
const { hasFrontmatter, hasKey, items } = walkScenarios(src)
|
|
142
|
+
if (!hasFrontmatter) return ['no frontmatter block — a yatsu.md must declare a `scenarios:` list']
|
|
143
|
+
if (!hasKey) return ['frontmatter has no `scenarios:` key — declare at least one scenario']
|
|
144
|
+
if (!items.length) return ['`scenarios:` declares no scenarios — add one (name + description + expected)']
|
|
145
|
+
const errs: string[] = []
|
|
146
|
+
const counts = new Map<string, number>()
|
|
147
|
+
items.forEach((it, idx) => {
|
|
148
|
+
const label = it.fields.name ? `scenario '${it.fields.name}'` : `scenario #${idx + 1}`
|
|
149
|
+
for (const k of ['name', 'description', 'expected'] as const) {
|
|
150
|
+
if (!it.fields[k]?.trim()) errs.push(`${label}: missing required field \`${k}\``)
|
|
151
|
+
}
|
|
152
|
+
for (const u of it.unknownKeys) errs.push(`${label}: unknown field \`${u}\` (allowed: ${SCENARIO_KEYS.join(', ')})`)
|
|
153
|
+
if (it.fields.name) counts.set(it.fields.name, (counts.get(it.fields.name) ?? 0) + 1)
|
|
154
|
+
})
|
|
155
|
+
for (const [n, c] of counts) if (c > 1) errs.push(`duplicate scenario name '${n}' (${c}×) — names must be unique within a yatsu.md`)
|
|
156
|
+
return errs
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// walk `.spec` for every dir holding a yatsu.md; a node's id is its leaf dir name (the same its spec.md carries)
|
|
160
|
+
export function yatsuNodes(root: string): YatsuNode[] {
|
|
161
|
+
const specDir = join(root, '.spec')
|
|
162
|
+
const out: YatsuNode[] = []
|
|
163
|
+
const stack = existsSync(specDir) ? [specDir] : []
|
|
164
|
+
while (stack.length) {
|
|
165
|
+
const dir = stack.pop()!
|
|
166
|
+
let ents
|
|
167
|
+
try { ents = readdirSync(dir, { withFileTypes: true }) } catch { continue }
|
|
168
|
+
if (existsSync(join(dir, YATSU_FILE))) {
|
|
169
|
+
const yatsuPath = relative(root, join(dir, YATSU_FILE))
|
|
170
|
+
out.push({
|
|
171
|
+
id: basename(dir),
|
|
172
|
+
dir,
|
|
173
|
+
yatsuPath,
|
|
174
|
+
sidecarPath: join(dir, SIDECAR_FILE),
|
|
175
|
+
scenarios: parseScenarios(readFileSync(join(dir, YATSU_FILE), 'utf8')),
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
for (const e of ents) if (e.isDirectory()) stack.push(join(dir, e.name))
|
|
179
|
+
}
|
|
180
|
+
return out.sort((a, b) => a.id.localeCompare(b.id))
|
|
181
|
+
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/{templates → spec-cli/templates}/spec/project/.config/core/mark-active/mark-active.sh
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/{templates → spec-cli/templates}/spec/project/.config/core/spec-first/spec-first.sh
RENAMED
|
File without changes
|
|
File without changes
|
/package/{templates → spec-cli/templates}/spec/project/.config/core/spec-of-file/spec-of-file.sh
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|