spexcode 0.1.1 → 0.1.2

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 (75) hide show
  1. package/package.json +17 -10
  2. package/{bin → spec-cli/bin}/spex.mjs +6 -2
  3. package/spec-cli/hooks/dispatch.sh +74 -0
  4. package/spec-cli/hooks/harness.sh +178 -0
  5. package/{src → spec-cli/src}/cli.ts +14 -0
  6. package/{src → spec-cli/src}/harness.ts +8 -18
  7. package/{src → spec-cli/src}/layout.ts +6 -23
  8. package/{src → spec-cli/src}/materialize.ts +2 -1
  9. package/spec-cli/src/relay.ts +28 -0
  10. package/{src → spec-cli/src}/supervise.ts +2 -1
  11. package/spec-cli/src/tsx-bin.ts +11 -0
  12. package/{dashboard-dist/assets/index-B60MILFg.js → spec-dashboard/dist/assets/index-Bk4E1EQy.js} +29 -29
  13. package/{dashboard-dist → spec-dashboard/dist}/index.html +1 -1
  14. package/spec-forge/src/__fixtures__/github-forge.json +63 -0
  15. package/spec-forge/src/cache.ts +28 -0
  16. package/spec-forge/src/cli.ts +110 -0
  17. package/spec-forge/src/drivers/github.ts +73 -0
  18. package/spec-forge/src/links.ts +76 -0
  19. package/spec-forge/src/needs-yatsu-eval.ts +30 -0
  20. package/spec-forge/src/port.ts +23 -0
  21. package/spec-forge/src/resident.ts +23 -0
  22. package/spec-yatsu/src/cache.ts +68 -0
  23. package/spec-yatsu/src/cli.ts +296 -0
  24. package/spec-yatsu/src/evaltab.ts +112 -0
  25. package/spec-yatsu/src/evaluator.ts +24 -0
  26. package/spec-yatsu/src/freshness.ts +51 -0
  27. package/spec-yatsu/src/proof.ts +491 -0
  28. package/spec-yatsu/src/sidecar.ts +44 -0
  29. package/spec-yatsu/src/yatsu.ts +181 -0
  30. /package/{src → spec-cli/src}/board.ts +0 -0
  31. /package/{src → spec-cli/src}/client.ts +0 -0
  32. /package/{src → spec-cli/src}/gateway.ts +0 -0
  33. /package/{src → spec-cli/src}/git.ts +0 -0
  34. /package/{src → spec-cli/src}/guide.ts +0 -0
  35. /package/{src → spec-cli/src}/hooks.ts +0 -0
  36. /package/{src → spec-cli/src}/index.ts +0 -0
  37. /package/{src → spec-cli/src}/init.ts +0 -0
  38. /package/{src → spec-cli/src}/lint.ts +0 -0
  39. /package/{src → spec-cli/src}/login-page.ts +0 -0
  40. /package/{src → spec-cli/src}/pty-bridge.ts +0 -0
  41. /package/{src → spec-cli/src}/ranker.ts +0 -0
  42. /package/{src → spec-cli/src}/resilience.ts +0 -0
  43. /package/{src → spec-cli/src}/search.bench.mjs +0 -0
  44. /package/{src → spec-cli/src}/search.ts +0 -0
  45. /package/{src → spec-cli/src}/self.ts +0 -0
  46. /package/{src → spec-cli/src}/sessions.ts +0 -0
  47. /package/{src → spec-cli/src}/slash-commands.ts +0 -0
  48. /package/{src → spec-cli/src}/specs.ts +0 -0
  49. /package/{src → spec-cli/src}/uploads.ts +0 -0
  50. /package/{templates → spec-cli/templates}/hooks/pre-commit +0 -0
  51. /package/{templates → spec-cli/templates}/hooks/prepare-commit-msg +0 -0
  52. /package/{templates → spec-cli/templates}/spec/project/.config/core/idle/idle.sh +0 -0
  53. /package/{templates → spec-cli/templates}/spec/project/.config/core/idle/spec.md +0 -0
  54. /package/{templates → spec-cli/templates}/spec/project/.config/core/mark-active/mark-active.sh +0 -0
  55. /package/{templates → spec-cli/templates}/spec/project/.config/core/mark-active/spec.md +0 -0
  56. /package/{templates → spec-cli/templates}/spec/project/.config/core/session-fail/fail.sh +0 -0
  57. /package/{templates → spec-cli/templates}/spec/project/.config/core/session-fail/spec.md +0 -0
  58. /package/{templates → spec-cli/templates}/spec/project/.config/core/spec-first/spec-first.sh +0 -0
  59. /package/{templates → spec-cli/templates}/spec/project/.config/core/spec-first/spec.md +0 -0
  60. /package/{templates → spec-cli/templates}/spec/project/.config/core/spec-of-file/spec-of-file.sh +0 -0
  61. /package/{templates → spec-cli/templates}/spec/project/.config/core/spec-of-file/spec.md +0 -0
  62. /package/{templates → spec-cli/templates}/spec/project/.config/core/spec.md +0 -0
  63. /package/{templates → spec-cli/templates}/spec/project/.config/core/stop-gate/spec.md +0 -0
  64. /package/{templates → spec-cli/templates}/spec/project/.config/core/stop-gate/stop-gate.sh +0 -0
  65. /package/{templates → spec-cli/templates}/spec/project/.config/extract/spec.md +0 -0
  66. /package/{templates → spec-cli/templates}/spec/project/.config/forge-link/spec.md +0 -0
  67. /package/{templates → spec-cli/templates}/spec/project/.config/memory-hygiene/spec.md +0 -0
  68. /package/{templates → spec-cli/templates}/spec/project/.config/regroup/spec.md +0 -0
  69. /package/{templates → spec-cli/templates}/spec/project/.config/scenario/spec.md +0 -0
  70. /package/{templates → spec-cli/templates}/spec/project/.config/spec.md +0 -0
  71. /package/{templates → spec-cli/templates}/spec/project/.config/supervisor/spec.md +0 -0
  72. /package/{templates → spec-cli/templates}/spec/project/.config/tidy/spec.md +0 -0
  73. /package/{templates → spec-cli/templates}/spec/project/spec.md +0 -0
  74. /package/{templates → spec-cli/templates}/spexcode.json +0 -0
  75. /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