@abarcenas/ai-workflow-template 1.48.0 → 2.0.0

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 (37) hide show
  1. package/.agents/instructions/knowledgebase.instructions.md +2 -2
  2. package/.agents/instructions/learned-knowledge.instructions.md +47 -2
  3. package/.agents/instructions/memory-schema.instructions.md +1 -1
  4. package/.github/workflows/npm-publish.yml +69 -1
  5. package/.opencode/agents/implementer.md +3 -1
  6. package/.opencode/agents/unit-tester.md +21 -2
  7. package/.opencode/plugins/graphify.js +22 -0
  8. package/LICENSE +15 -0
  9. package/README.md +56 -22
  10. package/memory-bank/.vocabulary.json +3 -0
  11. package/opencode.mcp.example.json +13 -0
  12. package/package.json +20 -10
  13. package/scripts/bump-version.js +18 -2
  14. package/scripts/mcp/playwright-mcp-launcher.js +45 -2
  15. package/scripts/bump-version.test.js +0 -238
  16. package/scripts/knowledgebase-cli.test.js +0 -576
  17. package/scripts/knowledgebase-index.test.js +0 -759
  18. package/scripts/mcp/playwright-mcp-launcher.test.js +0 -217
  19. package/scripts/mcp-knowledgebase-server.test.js +0 -745
  20. package/scripts/mcp-memory-server.test.js +0 -398
  21. package/scripts/memory-cli.test.js +0 -297
  22. package/scripts/memory-index.test.js +0 -1173
  23. package/scripts/normalize-memory.test.js +0 -753
  24. package/scripts/setup/discover.test.js +0 -530
  25. package/scripts/setup/hooks.test.js +0 -722
  26. package/scripts/setup/husky-init.test.js +0 -311
  27. package/scripts/setup/index.test.js +0 -1021
  28. package/scripts/setup/knowledgebase.test.js +0 -352
  29. package/scripts/setup/prepare.test.js +0 -718
  30. package/scripts/setup/skills-phase.test.js +0 -1274
  31. package/scripts/setup/sync-phase.test.js +0 -373
  32. package/scripts/setup/ui.test.js +0 -579
  33. package/scripts/setup/utils.test.js +0 -586
  34. package/scripts/sync-unit.test.js +0 -1881
  35. package/scripts/sync.test.js +0 -472
  36. package/scripts/validate-memory-schema.test.js +0 -328
  37. package/scripts/vocab-sync.test.js +0 -902
@@ -1,328 +0,0 @@
1
- // ═════════════════════════════════════════════════════════════════════════════
2
- // validate-memory-schema.test.js — Unit tests for the pre-commit validator
3
- // ═════════════════════════════════════════════════════════════════════════════
4
- //
5
- // Covers plan/feature-vocab-sync-1.md T10 (TEST-01..05) — the T3
6
- // entity_patterns awareness fix: `entity_patterns` keys (npm, vitest, …) count
7
- // as known tags, so they no longer produce false-positive "Unknown tags"
8
- // warnings; genuinely unknown tags still warn; and the validator keeps the
9
- // warn-don't-block contract (exit code stays 0 on warnings).
10
- //
11
- // The validator is exercised through its exported functions (testability seam
12
- // added in T10: `export { validateFile, main }` + `isDirectRun` guard, matching
13
- // scripts/vocab-sync.js / scripts/mcp-knowledgebase-server.js convention).
14
- // child_process.execSync (staged-file detection) and node:fs.readFileSync
15
- // (file + vocab content) are mocked hermetically so no git/FS side effects occur.
16
- // ═════════════════════════════════════════════════════════════════════════════
17
-
18
- import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'
19
-
20
- // Module-level mocks — the validator imports execSync from node:child_process
21
- // and readFileSync/existsSync from node:fs. vi.mock is hoisted above imports,
22
- // so the validator under test receives these mocked implementations.
23
- vi.mock('node:child_process', async () => {
24
- const actual = await vi.importActual('node:child_process')
25
- return { ...actual, execSync: vi.fn() }
26
- })
27
-
28
- vi.mock('node:fs', async () => {
29
- const actual = await vi.importActual('node:fs')
30
- return {
31
- ...actual,
32
- readFileSync: vi.fn(),
33
- existsSync: vi.fn(() => true),
34
- }
35
- })
36
-
37
- import { readFileSync } from 'node:fs'
38
- import { execSync } from 'node:child_process'
39
-
40
- import { validateFile, main } from './validate-memory-schema.js'
41
-
42
- // ── Fixtures ─────────────────────────────────────────────────────────
43
-
44
- const FILE_PATH = 'memory-bank/sample.md'
45
-
46
- function makeFileContent({
47
- id = 'sample',
48
- title = 'Sample',
49
- updated = '2026-08-03',
50
- tags = [],
51
- category = 'memory_ops',
52
- } = {}) {
53
- return [
54
- '---',
55
- `id: ${id}`,
56
- `title: "${title}"`,
57
- `updated: ${updated}`,
58
- `tags: [${tags.join(', ')}]`,
59
- 'entities: [sample]',
60
- `category: ${category}`,
61
- '---',
62
- '',
63
- '# Sample',
64
- '',
65
- 'Body.',
66
- '',
67
- ].join('\n')
68
- }
69
-
70
- // Mirrors the real memory-bank/.vocabulary.json shape: tags.* groups hold
71
- // known concept tags; entity_patterns keys hold component/tool names.
72
- const VOCAB = {
73
- categories: { memory_ops: 'Memory operations', context: 'Context files' },
74
- tags: {
75
- agent_roles: ['architect', 'implementer'],
76
- workflow: ['planning', 'review'],
77
- memory_ops: ['indexing'],
78
- topic: ['mcp', 'graphify'],
79
- },
80
- entity_patterns: {
81
- npm: { patterns: ['\\bnpm\\b'] },
82
- vitest: { patterns: ['\\bvitest\\b'] },
83
- playwright: { patterns: ['\\bplaywright\\b'] },
84
- },
85
- }
86
-
87
- // ── Test harness ─────────────────────────────────────────────────────
88
-
89
- let warnSpy
90
-
91
- beforeEach(() => {
92
- vi.clearAllMocks()
93
- warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
94
- })
95
-
96
- afterEach(() => {
97
- vi.restoreAllMocks()
98
- })
99
-
100
- // ═════════════════════════════════════════════════════════════════════════════
101
- // T10 scenarios — entity_patterns awareness (plan TEST-01..05)
102
- // ═════════════════════════════════════════════════════════════════════════════
103
-
104
- describe('validateFile — entity_patterns awareness (plan T10)', () => {
105
- it('TEST-01: does NOT warn when a tag is an entity_patterns key (npm)', () => {
106
- readFileSync.mockReturnValue(makeFileContent({ tags: ['npm'] }))
107
-
108
- const errors = validateFile(FILE_PATH, VOCAB)
109
-
110
- expect(warnSpy).not.toHaveBeenCalledWith(expect.stringContaining('Unknown tags'))
111
- expect(errors).toEqual([])
112
- })
113
-
114
- it('TEST-02: does NOT warn when a tag is in a tags.* group (mcp in tags.topic)', () => {
115
- readFileSync.mockReturnValue(makeFileContent({ tags: ['mcp'] }))
116
-
117
- const errors = validateFile(FILE_PATH, VOCAB)
118
-
119
- expect(warnSpy).not.toHaveBeenCalledWith(expect.stringContaining('Unknown tags'))
120
- expect(errors).toEqual([])
121
- })
122
-
123
- it('TEST-03: warns when a tag is genuinely unknown (some-random-tag)', () => {
124
- readFileSync.mockReturnValue(makeFileContent({ tags: ['some-random-tag'] }))
125
-
126
- const errors = validateFile(FILE_PATH, VOCAB)
127
-
128
- expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Unknown tags'))
129
- expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('some-random-tag'))
130
- // warn-don't-block: a warning is NOT a validation error
131
- expect(errors).toEqual([])
132
- })
133
-
134
- it('TEST-04: warns ONLY for truly unknown tags when mixed with entity_patterns keys', () => {
135
- readFileSync.mockReturnValue(
136
- makeFileContent({ tags: ['npm', 'vitest', 'some-random-tag'] })
137
- )
138
-
139
- const errors = validateFile(FILE_PATH, VOCAB)
140
-
141
- const warnText = warnSpy.mock.calls.map((call) => String(call[0])).join(' ')
142
- expect(warnText).toContain('Unknown tags')
143
- expect(warnText).toContain('some-random-tag')
144
- expect(warnText).not.toContain('npm')
145
- expect(warnText).not.toContain('vitest')
146
- expect(errors).toEqual([])
147
- })
148
-
149
- it('TEST-05: handles a vocab with no entity_patterns key gracefully (no crash)', () => {
150
- const vocabNoEntityPatterns = {
151
- categories: { memory_ops: 'Memory operations' },
152
- tags: { topic: ['mcp'] },
153
- // deliberately NO entity_patterns key
154
- }
155
- readFileSync.mockReturnValue(makeFileContent({ tags: ['npm'] }))
156
-
157
- // `Object.keys(vocab.entity_patterns || {})` must not throw on undefined
158
- expect(() => validateFile(FILE_PATH, vocabNoEntityPatterns)).not.toThrow()
159
- // without entity_patterns, 'npm' is genuinely unknown → degrades to a warning
160
- expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Unknown tags'))
161
- expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('npm'))
162
- })
163
-
164
- it('TEST-05b: does not crash with a completely empty vocab object ({})', () => {
165
- readFileSync.mockReturnValue(makeFileContent({ tags: ['whatever'] }))
166
-
167
- expect(() => validateFile(FILE_PATH, {})).not.toThrow()
168
- // no tags.* groups and no entity_patterns → tag check skipped, no warning
169
- expect(warnSpy).not.toHaveBeenCalledWith(expect.stringContaining('Unknown tags'))
170
- })
171
- })
172
-
173
- // ═════════════════════════════════════════════════════════════════════════════
174
- // Reviewer M1 — normalization parity with vocab-sync
175
- // ═════════════════════════════════════════════════════════════════════════════
176
- // vocab-sync.js normalizes tags (lowercase, kebab-case) before comparing and
177
- // appending, so after it appends `my-tag` a staged file whose frontmatter still
178
- // says `My Tag` must NOT warn. The validator therefore normalizes both sides
179
- // before the unknown-tag filter (reviewer finding M1). The three scenarios below
180
- // lock in: (a) normalized-known no-warn, (b) genuinely unknown still warns,
181
- // (c) mixed-case genuinely unknown still warns.
182
-
183
- describe('validateFile — tag normalization parity with vocab-sync (reviewer M1)', () => {
184
- it('M1-a: does NOT warn when a mixed-case staged tag normalizes to a known tag (My Tag → my-tag)', () => {
185
- const vocabWithNormalized = {
186
- ...VOCAB,
187
- tags: { ...VOCAB.tags, topic: [...VOCAB.tags.topic, 'my-tag'] },
188
- }
189
- readFileSync.mockReturnValue(makeFileContent({ tags: ['My Tag'] }))
190
-
191
- const errors = validateFile(FILE_PATH, vocabWithNormalized)
192
-
193
- expect(warnSpy).not.toHaveBeenCalledWith(expect.stringContaining('Unknown tags'))
194
- expect(errors).toEqual([])
195
- })
196
-
197
- it('M1-b: still warns when a tag is genuinely unknown after normalization (some-random-tag)', () => {
198
- readFileSync.mockReturnValue(makeFileContent({ tags: ['some-random-tag'] }))
199
-
200
- const errors = validateFile(FILE_PATH, VOCAB)
201
-
202
- expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Unknown tags'))
203
- expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('some-random-tag'))
204
- // warn-don't-block: a warning is NOT a validation error
205
- expect(errors).toEqual([])
206
- })
207
-
208
- it('M1-c: still warns for a mixed-case tag that is NOT in the vocabulary', () => {
209
- readFileSync.mockReturnValue(makeFileContent({ tags: ['Random Tag'] }))
210
-
211
- const errors = validateFile(FILE_PATH, VOCAB)
212
-
213
- expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Unknown tags'))
214
- expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Random Tag'))
215
- // warn-don't-block: a warning is NOT a validation error
216
- expect(errors).toEqual([])
217
- })
218
- })
219
-
220
- // ═════════════════════════════════════════════════════════════════════════════
221
- // main() — exit-code contract (warn-don't-block, REQ-05 / CON-02)
222
- // ═════════════════════════════════════════════════════════════════════════════
223
-
224
- describe('main() — exit code behavior', () => {
225
- it('exits 0 when unknown-tag warnings occur (warnings do NOT block the commit)', () => {
226
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {})
227
- execSync.mockReturnValue(`${FILE_PATH}\n`)
228
- readFileSync.mockImplementation((filePath) => {
229
- if (String(filePath).includes('.vocabulary.json')) return JSON.stringify(VOCAB)
230
- return makeFileContent({ tags: ['some-random-tag'] })
231
- })
232
-
233
- main()
234
-
235
- expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Unknown tags'))
236
- expect(exitSpy).toHaveBeenCalledWith(0)
237
- })
238
-
239
- it('exits 0 when no memory-bank files are staged', () => {
240
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {})
241
- execSync.mockReturnValue('')
242
-
243
- main()
244
-
245
- expect(exitSpy).toHaveBeenCalledWith(0)
246
- })
247
-
248
- it('exits 1 when validation errors are found', () => {
249
- const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {})
250
- execSync.mockReturnValue(`${FILE_PATH}\n`)
251
- readFileSync.mockImplementation((filePath) => {
252
- if (String(filePath).includes('.vocabulary.json')) return JSON.stringify(VOCAB)
253
- // id does not match the filename → real validation error
254
- return makeFileContent({ id: 'mismatched-id', tags: ['mcp'] })
255
- })
256
-
257
- main()
258
-
259
- expect(exitSpy).toHaveBeenCalledWith(1)
260
- })
261
- })
262
-
263
- // ═════════════════════════════════════════════════════════════════════════════
264
- // validateFile — error branches (robustness / coverage support)
265
- // ═════════════════════════════════════════════════════════════════════════════
266
-
267
- describe('validateFile — error branches', () => {
268
- it('returns a "Cannot read file" error when the file is unreadable', () => {
269
- readFileSync.mockImplementation(() => {
270
- throw new Error('ENOENT')
271
- })
272
-
273
- const errors = validateFile(FILE_PATH, VOCAB)
274
-
275
- expect(errors).toEqual([`Cannot read file: ${FILE_PATH}`])
276
- })
277
-
278
- it('reports missing YAML frontmatter', () => {
279
- readFileSync.mockReturnValue('# No frontmatter here\n')
280
-
281
- const errors = validateFile(FILE_PATH, VOCAB)
282
-
283
- expect(errors).toEqual(['Missing YAML frontmatter (file must start with ---...---)'])
284
- })
285
-
286
- it('reports missing required fields', () => {
287
- readFileSync.mockReturnValue('---\nid: sample\n---\n')
288
-
289
- const errors = validateFile(FILE_PATH, VOCAB)
290
-
291
- expect(errors).toEqual([
292
- "Missing required field: 'title'",
293
- "Missing required field: 'updated'",
294
- "Missing required field: 'tags'",
295
- "Missing required field: 'category'",
296
- ])
297
- })
298
-
299
- it('reports an id/filename mismatch', () => {
300
- readFileSync.mockReturnValue(makeFileContent({ id: 'mismatched-id' }))
301
-
302
- const errors = validateFile(FILE_PATH, VOCAB)
303
-
304
- expect(errors).toContain(
305
- `Field 'id' (mismatched-id) does not match filename (sample)`
306
- )
307
- })
308
-
309
- it('reports an invalid updated date', () => {
310
- readFileSync.mockReturnValue(makeFileContent({ updated: 'yesterday' }))
311
-
312
- const errors = validateFile(FILE_PATH, VOCAB)
313
-
314
- expect(errors).toContain(
315
- `Field 'updated' (yesterday) is not a valid date. Use YYYY-MM-DD format.`
316
- )
317
- })
318
-
319
- it('reports an invalid category', () => {
320
- readFileSync.mockReturnValue(makeFileContent({ category: 'not-a-category' }))
321
-
322
- const errors = validateFile(FILE_PATH, VOCAB)
323
-
324
- expect(errors).toContain(
325
- `Invalid category 'not-a-category'. Allowed: memory_ops, context`
326
- )
327
- })
328
- })