@abarcenas/ai-workflow-template 1.49.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 (35) 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/plugins/graphify.js +22 -0
  6. package/LICENSE +15 -0
  7. package/README.md +56 -22
  8. package/memory-bank/.vocabulary.json +3 -0
  9. package/opencode.mcp.example.json +13 -0
  10. package/package.json +20 -10
  11. package/scripts/bump-version.js +18 -2
  12. package/scripts/mcp/playwright-mcp-launcher.js +45 -2
  13. package/scripts/bump-version.test.js +0 -238
  14. package/scripts/knowledgebase-cli.test.js +0 -576
  15. package/scripts/knowledgebase-index.test.js +0 -759
  16. package/scripts/mcp/playwright-mcp-launcher.test.js +0 -217
  17. package/scripts/mcp-knowledgebase-server.test.js +0 -745
  18. package/scripts/mcp-memory-server.test.js +0 -398
  19. package/scripts/memory-cli.test.js +0 -297
  20. package/scripts/memory-index.test.js +0 -1173
  21. package/scripts/normalize-memory.test.js +0 -753
  22. package/scripts/setup/discover.test.js +0 -530
  23. package/scripts/setup/hooks.test.js +0 -722
  24. package/scripts/setup/husky-init.test.js +0 -311
  25. package/scripts/setup/index.test.js +0 -1021
  26. package/scripts/setup/knowledgebase.test.js +0 -352
  27. package/scripts/setup/prepare.test.js +0 -718
  28. package/scripts/setup/skills-phase.test.js +0 -1274
  29. package/scripts/setup/sync-phase.test.js +0 -373
  30. package/scripts/setup/ui.test.js +0 -579
  31. package/scripts/setup/utils.test.js +0 -586
  32. package/scripts/sync-unit.test.js +0 -1881
  33. package/scripts/sync.test.js +0 -472
  34. package/scripts/validate-memory-schema.test.js +0 -328
  35. 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
- })