spexcode 0.4.0 → 0.4.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.
Files changed (111) hide show
  1. package/README.md +37 -17
  2. package/package.json +9 -5
  3. package/spec-cli/README.md +23 -14
  4. package/spec-cli/hooks/dispatch.sh +7 -5
  5. package/spec-cli/hooks/harness.sh +19 -8
  6. package/spec-cli/src/anchors.ts +163 -30
  7. package/spec-cli/src/cli.ts +97 -42
  8. package/spec-cli/src/client.ts +34 -11
  9. package/spec-cli/src/doctor.ts +174 -36
  10. package/spec-cli/src/gateway-auth.ts +194 -0
  11. package/spec-cli/src/gateway-hub.ts +360 -0
  12. package/spec-cli/src/gateway.ts +31 -16
  13. package/spec-cli/src/git.ts +12 -5
  14. package/spec-cli/src/graph.ts +53 -36
  15. package/spec-cli/src/graphStream.ts +265 -34
  16. package/spec-cli/src/guide.ts +157 -76
  17. package/spec-cli/src/harness-select.ts +20 -6
  18. package/spec-cli/src/harness.ts +266 -67
  19. package/spec-cli/src/help.ts +84 -41
  20. package/spec-cli/src/host.ts +610 -0
  21. package/spec-cli/src/identity-presets.d.ts +13 -0
  22. package/spec-cli/src/identity-presets.js +128 -0
  23. package/spec-cli/src/index.ts +163 -86
  24. package/spec-cli/src/init.ts +53 -25
  25. package/spec-cli/src/layout.ts +5 -5
  26. package/spec-cli/src/lint.ts +115 -169
  27. package/spec-cli/src/login-page.ts +12 -5
  28. package/spec-cli/src/materialize.ts +39 -13
  29. package/spec-cli/src/mentions.ts +16 -11
  30. package/spec-cli/src/opencode.ts +145 -0
  31. package/spec-cli/src/pi-harness.ts +124 -0
  32. package/spec-cli/src/project-identity.ts +73 -0
  33. package/spec-cli/src/pty-bridge.ts +321 -521
  34. package/spec-cli/src/pty-helper.mjs +86 -0
  35. package/spec-cli/src/reaper.ts +16 -8
  36. package/spec-cli/src/reviewSnapshot.ts +23 -0
  37. package/spec-cli/src/reviews.ts +347 -0
  38. package/spec-cli/src/session-timeline.ts +162 -0
  39. package/spec-cli/src/sessions.ts +210 -63
  40. package/spec-cli/src/shim-runtime.ts +169 -0
  41. package/spec-cli/src/slash-commands.ts +76 -0
  42. package/spec-cli/src/source-files.ts +79 -0
  43. package/spec-cli/src/specs.ts +47 -24
  44. package/spec-cli/src/supervise.ts +37 -15
  45. package/spec-cli/src/tree.ts +8 -8
  46. package/spec-cli/src/uninstall.ts +56 -23
  47. package/spec-cli/templates/hooks/prepare-commit-msg +32 -13
  48. package/spec-cli/templates/spec/project/.plugins/{extract → commands/extract}/spec.md +24 -18
  49. package/spec-cli/templates/spec/project/.plugins/{regroup → commands/regroup}/spec.md +3 -3
  50. package/spec-cli/templates/spec/project/.plugins/commands/rename/spec.md +11 -0
  51. package/spec-cli/templates/spec/project/.plugins/commands/spec.md +30 -0
  52. package/spec-cli/templates/spec/project/.plugins/commands/supervisor/spec.md +8 -0
  53. package/spec-cli/templates/spec/project/.plugins/commands/tidy/spec.md +43 -0
  54. package/spec-cli/templates/spec/project/.plugins/core/idle/spec.md +1 -1
  55. package/spec-cli/templates/spec/project/.plugins/core/mark-active/spec.md +2 -0
  56. package/spec-cli/templates/spec/project/.plugins/core/session-fail/spec.md +1 -1
  57. package/spec-cli/templates/spec/project/.plugins/core/spec-first/spec-first.sh +28 -39
  58. package/spec-cli/templates/spec/project/.plugins/core/spec-first/spec.md +5 -3
  59. package/spec-cli/templates/spec/project/.plugins/core/spec-of-file/spec.md +1 -1
  60. package/spec-cli/templates/spec/project/.plugins/core/spec.md +2 -2
  61. package/spec-cli/templates/spec/project/.plugins/core/stop-gate/spec.md +7 -1
  62. package/spec-cli/templates/spec/project/.plugins/prompts/forge-link/spec.md +2 -2
  63. package/spec-cli/templates/spec/project/.plugins/prompts/reproduce-before-fix/spec.md +5 -5
  64. package/spec-cli/templates/spec/project/.plugins/prompts/spec.md +10 -6
  65. package/spec-cli/templates/spec/project/.plugins/skills/distill/spec.md +62 -0
  66. package/spec-cli/templates/spec/project/.plugins/skills/spec.md +19 -0
  67. package/spec-cli/templates/spec/project/.plugins/spec.md +17 -12
  68. package/spec-cli/templates/spec/project/spec.md +6 -5
  69. package/spec-cli/templates/spexcode.json +4 -2
  70. package/spec-dashboard/dist/assets/Dashboard-CiHh-gLD.js +27 -0
  71. package/spec-dashboard/dist/assets/EvalsPage-DmiX3rdU.js +2 -0
  72. package/spec-dashboard/dist/assets/IssuesPage-CIbVGRUJ.js +1 -0
  73. package/spec-dashboard/dist/assets/MobileApp-D-N9_eh0.js +2 -0
  74. package/spec-dashboard/dist/assets/Modal-DHMzSFJ4.js +1 -0
  75. package/spec-dashboard/dist/assets/PageScroll-C15adEYI.js +1 -0
  76. package/spec-dashboard/dist/assets/ProjectsPage-sQpzglp5.js +1 -0
  77. package/spec-dashboard/dist/assets/SessionInterface-6GBZ9nXN.css +32 -0
  78. package/spec-dashboard/dist/assets/SessionInterface-B8pGU7Rg.js +39 -0
  79. package/spec-dashboard/dist/assets/SessionWindow-CuDO_67z.js +13 -0
  80. package/spec-dashboard/dist/assets/Settings-C_N1wX1f.js +1 -0
  81. package/spec-dashboard/dist/assets/bindings-BC9vqpYU.js +1 -0
  82. package/spec-dashboard/dist/assets/index-DmWbmvCq.js +41 -0
  83. package/spec-dashboard/dist/assets/index-GGIVdKwH.css +1 -0
  84. package/spec-dashboard/dist/assets/terminalFont-DmTPNdbU.js +1 -0
  85. package/spec-dashboard/dist/index.html +10 -10
  86. package/spec-dashboard/src/reviewFilters.js +279 -0
  87. package/spec-dashboard/src/reviewQuery.js +174 -0
  88. package/spec-dashboard/src/session.js +167 -0
  89. package/spec-eval/src/cli.ts +80 -45
  90. package/spec-eval/src/evaltab.ts +4 -3
  91. package/spec-eval/src/humanok.ts +1 -1
  92. package/spec-eval/src/matrix.ts +693 -0
  93. package/spec-eval/src/scenarios.ts +116 -4
  94. package/spec-eval/src/sessioneval.ts +726 -66
  95. package/spec-cli/src/migrate-table.ts +0 -398
  96. package/spec-cli/src/migrate.ts +0 -386
  97. package/spec-cli/templates/spec/project/.plugins/distill/spec.md +0 -74
  98. package/spec-cli/templates/spec/project/.plugins/supervisor/spec.md +0 -8
  99. package/spec-cli/templates/spec/project/.plugins/tidy/spec.md +0 -25
  100. package/spec-dashboard/dist/assets/Dashboard-CTcH2eW9.js +0 -27
  101. package/spec-dashboard/dist/assets/EvalsPage-CJNKwHLN.js +0 -2
  102. package/spec-dashboard/dist/assets/FoldToggle-CVFbBpyW.js +0 -1
  103. package/spec-dashboard/dist/assets/IssuesPage-kULjonqj.js +0 -1
  104. package/spec-dashboard/dist/assets/MobileApp-B0ZJju8K.js +0 -1
  105. package/spec-dashboard/dist/assets/SessionInterface-BRKJqU2U.js +0 -71
  106. package/spec-dashboard/dist/assets/SessionInterface-DYP7pi_n.css +0 -32
  107. package/spec-dashboard/dist/assets/SessionWindow-CDhEL7wO.js +0 -9
  108. package/spec-dashboard/dist/assets/Settings-BL6FV_8S.js +0 -1
  109. package/spec-dashboard/dist/assets/index-DmQsNYKK.css +0 -1
  110. package/spec-dashboard/dist/assets/index-DulGPk6A.js +0 -41
  111. /package/spec-cli/templates/spec/project/.plugins/{distill → skills/distill}/digest.mjs +0 -0
@@ -7,12 +7,17 @@ import { mintIds } from '../../spec-cli/src/specs.js'
7
7
  export const EVAL_FILE = 'eval.md'
8
8
  export const SIDECAR_FILE = 'evals.ndjson'
9
9
 
10
+ export type ScenarioTestReference = {
11
+ path: string
12
+ name?: string
13
+ }
14
+
10
15
  export type Scenario = {
11
16
  name: string
12
17
  description: string
13
18
  expected: string
14
19
  tags?: string[]
15
- test?: string
20
+ test?: ScenarioTestReference
16
21
  code?: string[]
17
22
  related?: string[]
18
23
  }
@@ -28,10 +33,19 @@ export type EvalNode = {
28
33
  const SCENARIO_KEYS = ['name', 'description', 'expected', 'tags', 'test', 'code', 'related'] as const
29
34
  type ScenarioKey = (typeof SCENARIO_KEYS)[number]
30
35
  const LIST_KEYS: readonly ScenarioKey[] = ['tags', 'code', 'related']
36
+ const TEST_KEYS = ['path', 'name'] as const
37
+ type TestKey = (typeof TEST_KEYS)[number]
38
+
39
+ type RawTestObject = {
40
+ fields: Partial<Record<TestKey, string>>
41
+ unknownKeys: string[]
42
+ duplicateKeys: string[]
43
+ malformed: string[]
44
+ }
31
45
 
32
46
  // a raw scenario item straight off the frontmatter walk: the known fields it set, plus any UNKNOWN keys it
33
47
  // carried — kept (not dropped) so the validator can name a typo'd field instead of silently swallowing it.
34
- type RawItem = { fields: Partial<Record<ScenarioKey, string>>; unknownKeys: string[] }
48
+ type RawItem = { fields: Partial<Record<ScenarioKey, string>>; testObject?: RawTestObject; unknownKeys: string[] }
35
49
 
36
50
  // tiny indentation parser for eval.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"
37
51
  function walkScenarios(src: string): { hasFrontmatter: boolean; hasKey: boolean; items: RawItem[] } {
@@ -75,6 +89,32 @@ function assignField(cur: RawItem, kv: string, lines: string[], idx: number, key
75
89
  const f = kv.match(/^([A-Za-z_][\w-]*):\s*(.*)$/)
76
90
  if (!f) return idx
77
91
  const key = f[1]
92
+ if (key === 'test') {
93
+ const raw = f[2].trim()
94
+ if (!raw) {
95
+ const parsed = emptyTestObject()
96
+ let childIndent = -1
97
+ let j = idx + 1
98
+ for (; j < lines.length; j++) {
99
+ const line = lines[j]
100
+ if (!line.trim()) continue
101
+ const indent = line.length - line.replace(/^\s+/, '').length
102
+ if (indent <= keyIndent) break
103
+ if (childIndent < 0) childIndent = indent
104
+ if (indent !== childIndent) {
105
+ parsed.malformed.push(`invalid nested test object entry \`${line.trim()}\``)
106
+ continue
107
+ }
108
+ assignTestField(parsed, line.trim())
109
+ }
110
+ cur.testObject = parsed
111
+ return j - 1
112
+ }
113
+ if (raw.startsWith('{') || raw.endsWith('}')) {
114
+ cur.testObject = parseFlowTestObject(raw)
115
+ return idx
116
+ }
117
+ }
78
118
  // 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
79
119
  if ((LIST_KEYS as readonly string[]).includes(key) && f[2].trim() === '') {
80
120
  const items: string[] = []
@@ -118,6 +158,65 @@ function assignField(cur: RawItem, kv: string, lines: string[], idx: number, key
118
158
 
119
159
  const unquote = (s: string) => s.replace(/^["'](.*)["']$/, '$1').trim()
120
160
 
161
+ const emptyTestObject = (): RawTestObject => ({ fields: {}, unknownKeys: [], duplicateKeys: [], malformed: [] })
162
+
163
+ function testValue(raw: string, opaque = false): string {
164
+ const value = raw.trim()
165
+ const quoted = value.match(/^(["'])([\s\S]*)\1$/)
166
+ return quoted ? quoted[2] : opaque ? value : unquote(value)
167
+ }
168
+
169
+ function assignTestField(obj: RawTestObject, entry: string): void {
170
+ const f = entry.match(/^([A-Za-z_][\w-]*):\s*(.*)$/)
171
+ if (!f) { obj.malformed.push(`invalid test object entry \`${entry}\``); return }
172
+ const key = f[1]
173
+ if (!(TEST_KEYS as readonly string[]).includes(key)) { obj.unknownKeys.push(key); return }
174
+ if (key in obj.fields) obj.duplicateKeys.push(key)
175
+ obj.fields[key as TestKey] = testValue(f[2], key === 'name')
176
+ }
177
+
178
+ // A flow mapping is accepted beside the more readable block form. Split only on commas outside quotes so
179
+ // an opaque case name such as "allows admin, user" survives byte-for-byte after YAML quote removal.
180
+ function parseFlowTestObject(raw: string): RawTestObject {
181
+ const obj = emptyTestObject()
182
+ if (!raw.startsWith('{') || !raw.endsWith('}')) {
183
+ obj.malformed.push('`test` object must be a mapping with exactly `path` and `name`')
184
+ return obj
185
+ }
186
+ const body = raw.slice(1, -1).trim()
187
+ if (!body) return obj
188
+ const entries: string[] = []
189
+ let start = 0
190
+ let quote = ''
191
+ let escaped = false
192
+ for (let i = 0; i < body.length; i++) {
193
+ const ch = body[i]
194
+ if (quote) {
195
+ if (escaped) { escaped = false; continue }
196
+ if (ch === '\\') { escaped = true; continue }
197
+ if (ch === quote) quote = ''
198
+ continue
199
+ }
200
+ if (ch === '"' || ch === "'") { quote = ch; continue }
201
+ if (ch === ',') { entries.push(body.slice(start, i).trim()); start = i + 1 }
202
+ }
203
+ if (quote) obj.malformed.push('`test` object has an unterminated quoted value')
204
+ entries.push(body.slice(start).trim())
205
+ for (const entry of entries) assignTestField(obj, entry)
206
+ return obj
207
+ }
208
+
209
+ function normalizedTest(it: RawItem): ScenarioTestReference | undefined {
210
+ if (it.testObject) {
211
+ const path = it.testObject.fields.path
212
+ if (!path) return undefined
213
+ const name = it.testObject.fields.name
214
+ return { path, ...(name ? { name } : {}) }
215
+ }
216
+ const path = it.fields.test
217
+ return path ? { path } : undefined
218
+ }
219
+
121
220
  // @@@scenario contract hash - the deterministic content hash of a scenario's SEMANTIC text, stamped on each
122
221
  // reading at filing time ([[eval-core]]'s scenario freshness axis). Hashes ONLY the measurement contract —
123
222
  // description (what to measure) + expected (what zero loss looks like) — never name/tags/test/code/related.
@@ -144,12 +243,13 @@ export function parseScenarios(src: string): Scenario[] {
144
243
  const tags = it.fields.tags ? parseCodeList(it.fields.tags) : []
145
244
  const code = it.fields.code ? parseCodeList(it.fields.code) : []
146
245
  const related = it.fields.related ? parseCodeList(it.fields.related) : []
246
+ const test = normalizedTest(it)
147
247
  return {
148
248
  name: it.fields.name ?? '',
149
249
  description: it.fields.description ?? '',
150
250
  expected: it.fields.expected ?? '',
151
251
  ...(tags.length ? { tags } : {}),
152
- ...(it.fields.test ? { test: it.fields.test } : {}),
252
+ ...(test ? { test } : {}),
153
253
  ...(code.length ? { code } : {}),
154
254
  ...(related.length ? { related } : {}),
155
255
  }
@@ -161,7 +261,7 @@ export function parseScenarios(src: string): Scenario[] {
161
261
  // Every scenario needs ≥1 tag; each tag must be IN the library — an out-of-library tag is rejected LOUD with
162
262
  // the repair the user owns: pick an existing tag, or extend the library. An empty library (none configured)
163
263
  // disables only the membership check, never the ≥1-tag requirement.
164
- export function validateScenarios(src: string, tagLibrary: string[] = []): string[] {
264
+ export function validateScenarios(src: string, tagLibrary: string[] = [], pathRoot?: string): string[] {
165
265
  const { hasFrontmatter, hasKey, items } = walkScenarios(src)
166
266
  if (!hasFrontmatter) return ['no frontmatter block — an eval.md must declare a `scenarios:` list']
167
267
  if (!hasKey) return ['frontmatter has no `scenarios:` key — declare at least one scenario']
@@ -183,6 +283,18 @@ export function validateScenarios(src: string, tagLibrary: string[] = []): strin
183
283
  }
184
284
  }
185
285
  for (const u of it.unknownKeys) errs.push(`${label}: unknown field \`${u}\` (allowed: ${SCENARIO_KEYS.join(', ')})`)
286
+ if (it.testObject) {
287
+ for (const u of it.testObject.unknownKeys) errs.push(`${label}: unknown \`test\` field \`${u}\` (allowed: ${TEST_KEYS.join(', ')})`)
288
+ for (const d of it.testObject.duplicateKeys) errs.push(`${label}: duplicate \`test.${d}\` field`)
289
+ for (const e of it.testObject.malformed) errs.push(`${label}: ${e}`)
290
+ for (const k of TEST_KEYS) if (!it.testObject.fields[k]?.length) errs.push(`${label}: \`test\` object missing required field \`${k}\``)
291
+ } else if ('test' in it.fields && !it.fields.test?.length) {
292
+ errs.push(`${label}: \`test\` scalar path must not be empty`)
293
+ }
294
+ const test = normalizedTest(it)
295
+ if (test && pathRoot && !existsSync(join(pathRoot, test.path))) {
296
+ errs.push(`${label}: \`test.path\` not found: ${test.path}`)
297
+ }
186
298
  if (it.fields.name) counts.set(it.fields.name, (counts.get(it.fields.name) ?? 0) + 1)
187
299
  })
188
300
  for (const [n, c] of counts) if (c > 1) errs.push(`duplicate scenario name '${n}' (${c}×) — names must be unique within an eval.md`)