@techninja/clearstack 0.2.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 (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +81 -0
  3. package/bin/cli.js +62 -0
  4. package/docs/BACKEND_API_SPEC.md +281 -0
  5. package/docs/BUILD_LOG.md +193 -0
  6. package/docs/COMPONENT_PATTERNS.md +481 -0
  7. package/docs/CONVENTIONS.md +226 -0
  8. package/docs/FRONTEND_IMPLEMENTATION_RULES.md +239 -0
  9. package/docs/JSDOC_TYPING.md +86 -0
  10. package/docs/QUICKSTART.md +190 -0
  11. package/docs/SERVER_AND_DEPS.md +163 -0
  12. package/docs/STATE_AND_ROUTING.md +363 -0
  13. package/docs/TESTING.md +268 -0
  14. package/docs/app-spec/ENTITIES.md +37 -0
  15. package/docs/app-spec/README.md +19 -0
  16. package/lib/check.js +115 -0
  17. package/lib/copy.js +43 -0
  18. package/lib/init.js +73 -0
  19. package/lib/package-gen.js +83 -0
  20. package/lib/update.js +73 -0
  21. package/package.json +69 -0
  22. package/templates/fullstack/data/seed.json +1 -0
  23. package/templates/fullstack/src/api/db.js +75 -0
  24. package/templates/fullstack/src/api/entities.js +114 -0
  25. package/templates/fullstack/src/api/events.js +35 -0
  26. package/templates/fullstack/src/api/schemas.js +104 -0
  27. package/templates/fullstack/src/api/validate.js +52 -0
  28. package/templates/fullstack/src/pages/home/home-view.js +19 -0
  29. package/templates/fullstack/src/router/index.js +16 -0
  30. package/templates/fullstack/src/server.js +46 -0
  31. package/templates/fullstack/src/store/AppState.js +33 -0
  32. package/templates/fullstack/src/store/UserPrefs.js +31 -0
  33. package/templates/fullstack/src/store/realtimeSync.js +54 -0
  34. package/templates/shared/.configs/.prettierrc +8 -0
  35. package/templates/shared/.configs/eslint.config.js +64 -0
  36. package/templates/shared/.configs/jsconfig.json +24 -0
  37. package/templates/shared/.configs/web-test-runner.config.js +8 -0
  38. package/templates/shared/.env +9 -0
  39. package/templates/shared/.github/ISSUE_TEMPLATE/bug_report.md +42 -0
  40. package/templates/shared/.github/ISSUE_TEMPLATE/feature_request.md +30 -0
  41. package/templates/shared/.github/ISSUE_TEMPLATE/spec_correction.md +26 -0
  42. package/templates/shared/.github/pull_request_template.md +51 -0
  43. package/templates/shared/.github/workflows/spec.yml +46 -0
  44. package/templates/shared/README.md +22 -0
  45. package/templates/shared/docs/app-spec/README.md +40 -0
  46. package/templates/shared/docs/clearstack/BACKEND_API_SPEC.md +281 -0
  47. package/templates/shared/docs/clearstack/BUILD_LOG.md +193 -0
  48. package/templates/shared/docs/clearstack/COMPONENT_PATTERNS.md +481 -0
  49. package/templates/shared/docs/clearstack/CONVENTIONS.md +226 -0
  50. package/templates/shared/docs/clearstack/FRONTEND_IMPLEMENTATION_RULES.md +239 -0
  51. package/templates/shared/docs/clearstack/JSDOC_TYPING.md +86 -0
  52. package/templates/shared/docs/clearstack/QUICKSTART.md +190 -0
  53. package/templates/shared/docs/clearstack/SERVER_AND_DEPS.md +163 -0
  54. package/templates/shared/docs/clearstack/STATE_AND_ROUTING.md +363 -0
  55. package/templates/shared/docs/clearstack/TESTING.md +268 -0
  56. package/templates/shared/public/index.html +26 -0
  57. package/templates/shared/scripts/build-icons.js +86 -0
  58. package/templates/shared/scripts/vendor-deps.js +25 -0
  59. package/templates/shared/src/components/atoms/app-badge/app-badge.css +4 -0
  60. package/templates/shared/src/components/atoms/app-badge/app-badge.js +23 -0
  61. package/templates/shared/src/components/atoms/app-badge/app-badge.test.js +26 -0
  62. package/templates/shared/src/components/atoms/app-badge/index.js +1 -0
  63. package/templates/shared/src/components/atoms/app-button/app-button.css +3 -0
  64. package/templates/shared/src/components/atoms/app-button/app-button.js +41 -0
  65. package/templates/shared/src/components/atoms/app-button/app-button.test.js +43 -0
  66. package/templates/shared/src/components/atoms/app-button/index.js +1 -0
  67. package/templates/shared/src/components/atoms/app-icon/app-icon.css +4 -0
  68. package/templates/shared/src/components/atoms/app-icon/app-icon.js +57 -0
  69. package/templates/shared/src/components/atoms/app-icon/app-icon.test.js +30 -0
  70. package/templates/shared/src/components/atoms/app-icon/index.js +1 -0
  71. package/templates/shared/src/components/atoms/theme-toggle/index.js +1 -0
  72. package/templates/shared/src/components/atoms/theme-toggle/theme-toggle.css +10 -0
  73. package/templates/shared/src/components/atoms/theme-toggle/theme-toggle.js +42 -0
  74. package/templates/shared/src/styles/buttons.css +79 -0
  75. package/templates/shared/src/styles/components.css +31 -0
  76. package/templates/shared/src/styles/forms.css +20 -0
  77. package/templates/shared/src/styles/reset.css +32 -0
  78. package/templates/shared/src/styles/shared.css +135 -0
  79. package/templates/shared/src/styles/tokens.css +65 -0
  80. package/templates/shared/src/utils/formatDate.js +41 -0
  81. package/templates/shared/src/utils/statusColors.js +60 -0
  82. package/templates/static/src/pages/home/home-view.js +38 -0
  83. package/templates/static/src/router/index.js +16 -0
  84. package/templates/static/src/store/AppState.js +26 -0
@@ -0,0 +1,104 @@
1
+ /**
2
+ * JSON Schema + form layout registry for all entities.
3
+ * Each entry has { schema, layout } where layout defines form structure.
4
+ * @module api/schemas
5
+ */
6
+
7
+ /** @type {Map<string, { schema: object, layout: object }>} */
8
+ export const schemas = new Map();
9
+
10
+ schemas.set('projects', {
11
+ schema: {
12
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
13
+ title: 'Project',
14
+ type: 'object',
15
+ required: ['name'],
16
+ properties: {
17
+ id: { type: 'string', format: 'uuid', readOnly: true },
18
+ name: {
19
+ type: 'string',
20
+ minLength: 1,
21
+ maxLength: 200,
22
+ title: 'Name',
23
+ description: 'Project name',
24
+ placeholder: 'My Project',
25
+ },
26
+ description: {
27
+ type: 'string',
28
+ maxLength: 1000,
29
+ default: '',
30
+ title: 'Description',
31
+ description: 'Brief summary of the project',
32
+ },
33
+ status: {
34
+ type: 'string',
35
+ enum: ['active', 'archived'],
36
+ enumTitles: ['Active', 'Archived'],
37
+ default: 'active',
38
+ title: 'Status',
39
+ description: 'Current project status',
40
+ },
41
+ createdAt: { type: 'string', format: 'date-time', readOnly: true },
42
+ },
43
+ },
44
+ layout: {
45
+ groups: [
46
+ { fields: ['name'], columns: 1 },
47
+ { fields: ['description'], columns: 1 },
48
+ { fields: ['status'], columns: 1 },
49
+ ],
50
+ actions: { align: 'right' },
51
+ },
52
+ });
53
+
54
+ schemas.set('tasks', {
55
+ schema: {
56
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
57
+ title: 'Task',
58
+ type: 'object',
59
+ required: ['title', 'projectId'],
60
+ properties: {
61
+ id: { type: 'string', format: 'uuid', readOnly: true },
62
+ projectId: {
63
+ type: 'string',
64
+ format: 'uuid',
65
+ title: 'Project',
66
+ description: 'Parent project ID',
67
+ writeOnly: true,
68
+ },
69
+ title: {
70
+ type: 'string',
71
+ minLength: 1,
72
+ maxLength: 200,
73
+ title: 'Title',
74
+ description: 'Task title',
75
+ placeholder: 'What needs to be done?',
76
+ },
77
+ status: {
78
+ type: 'string',
79
+ enum: ['todo', 'doing', 'done'],
80
+ enumTitles: ['To Do', 'In Progress', 'Done'],
81
+ default: 'todo',
82
+ title: 'Status',
83
+ description: 'Current progress',
84
+ },
85
+ priority: {
86
+ type: 'string',
87
+ enum: ['low', 'med', 'high'],
88
+ enumTitles: ['Low', 'Medium', 'High'],
89
+ default: 'med',
90
+ title: 'Priority',
91
+ description: 'Urgency level',
92
+ },
93
+ sortOrder: { type: 'number', default: 0, writeOnly: true },
94
+ createdAt: { type: 'string', format: 'date-time', readOnly: true },
95
+ },
96
+ },
97
+ layout: {
98
+ groups: [
99
+ { fields: ['title'], columns: 1 },
100
+ { fields: ['status', 'priority'], columns: 2 },
101
+ ],
102
+ actions: { align: 'right' },
103
+ },
104
+ });
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Server-side validation against JSON Schema.
3
+ * Returns field-level errors for 422 responses.
4
+ * @module api/validate
5
+ */
6
+
7
+ import { schemas } from './db.js';
8
+
9
+ /**
10
+ * Validate a request body against the entity's JSON Schema.
11
+ * @param {string} entity - Entity name, e.g. 'projects'
12
+ * @param {object} body - Request body to validate
13
+ * @param {boolean} [partial=false] - If true, skip required checks for missing keys
14
+ * @returns {{ valid: boolean, fields: Record<string, string> }}
15
+ */
16
+ export function validate(entity, body, partial = false) {
17
+ const entry = schemas.get(entity);
18
+ if (!entry) return { valid: true, fields: /** @type {Record<string, string>} */ ({}) };
19
+
20
+ /** @type {Record<string, string>} */
21
+ const fields = {};
22
+ const props = entry.schema.properties || {};
23
+ const required = entry.schema.required || [];
24
+
25
+ if (!partial) {
26
+ for (const name of required) {
27
+ const val = body[name];
28
+ if (val === undefined || val === null || val === '') {
29
+ fields[name] = `${name} is required`;
30
+ }
31
+ }
32
+ }
33
+
34
+ for (const [name, val] of Object.entries(body)) {
35
+ const prop = props[name];
36
+ if (!prop || prop.readOnly) continue;
37
+
38
+ if (typeof val === 'string') {
39
+ if (prop.minLength && val.length < prop.minLength) {
40
+ fields[name] = `Minimum ${prop.minLength} characters`;
41
+ }
42
+ if (prop.maxLength && val.length > prop.maxLength) {
43
+ fields[name] = `Maximum ${prop.maxLength} characters`;
44
+ }
45
+ if (prop.enum && !prop.enum.includes(val)) {
46
+ fields[name] = `Must be one of: ${prop.enum.join(', ')}`;
47
+ }
48
+ }
49
+ }
50
+
51
+ return { valid: Object.keys(fields).length === 0, fields };
52
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Home page — starting point for your app.
3
+ * @module pages/home
4
+ */
5
+
6
+ import { html, define } from 'hybrids';
7
+
8
+ export default define({
9
+ tag: 'home-view',
10
+ render: {
11
+ value: () => html`
12
+ <div class="home-view">
13
+ <h1>{{name}}</h1>
14
+ <p>Your Clearstack project is ready. Start building!</p>
15
+ </div>
16
+ `,
17
+ shadow: false,
18
+ },
19
+ });
@@ -0,0 +1,16 @@
1
+ /**
2
+ * App router shell — manages view stack and realtime sync.
3
+ * @module router
4
+ */
5
+
6
+ import { html, define, router } from 'hybrids';
7
+ import HomeView from '../pages/home/home-view.js';
8
+
9
+ export default define({
10
+ tag: 'app-router',
11
+ stack: router(HomeView, { url: '/' }),
12
+ render: {
13
+ value: ({ stack }) => html`<div class="app-router">${stack}</div>`,
14
+ shadow: false,
15
+ },
16
+ });
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Express server entry point.
3
+ * @module server
4
+ */
5
+
6
+ import express from 'express';
7
+ import { entityRouter } from './api/entities.js';
8
+ import { eventsRouter } from './api/events.js';
9
+
10
+ const app = express();
11
+
12
+ app.use(express.json());
13
+ app.use(express.static('public'));
14
+ app.use('/src', express.static('src'));
15
+
16
+ app.use('/api', eventsRouter);
17
+ app.use('/api', entityRouter);
18
+
19
+ // SPA fallback
20
+ app.use((req, res, next) => {
21
+ if (req.method === 'GET' && !req.path.includes('.')) {
22
+ return res.sendFile('index.html', { root: 'public' });
23
+ }
24
+ next();
25
+ });
26
+
27
+ /**
28
+ * Start the server.
29
+ * @param {number} [port=3000]
30
+ * @returns {import('node:http').Server}
31
+ */
32
+ export function start(port = {{port}}) {
33
+ const server = app.listen(port, () => console.log(`http://localhost:${port}`));
34
+ server.on('error', (/** @type {NodeJS.ErrnoException} */ err) => {
35
+ if (err.code === 'EADDRINUSE') console.error(`Port ${port} in use. Try: PORT=4354 npm start`);
36
+ else console.error(err);
37
+ process.exit(1);
38
+ });
39
+ return server;
40
+ }
41
+
42
+ if (import.meta.url === `file://${process.argv[1]}`) {
43
+ start(parseInt(process.env.PORT) || {{port}});
44
+ }
45
+
46
+ export default app;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Global UI state — singleton, persisted to localStorage.
3
+ * Theme, sidebar, active filters. No API calls.
4
+ * @module store/AppState
5
+ */
6
+
7
+ import { store } from 'hybrids';
8
+
9
+ /**
10
+ * @typedef {Object} AppState
11
+ * @property {string} theme - 'light' or 'dark'
12
+ * @property {boolean} sidebarOpen - Sidebar visibility
13
+ * @property {string} activeFilter - Current task filter value
14
+ */
15
+
16
+ /** @type {import('hybrids').Model<AppState>} */
17
+ const AppState = {
18
+ theme: 'light',
19
+ sidebarOpen: true,
20
+ activeFilter: 'all',
21
+ [store.connect]: {
22
+ get: () => {
23
+ const raw = localStorage.getItem('appState');
24
+ return raw ? JSON.parse(raw) : {};
25
+ },
26
+ set: (id, values) => {
27
+ localStorage.setItem('appState', JSON.stringify(values));
28
+ return values;
29
+ },
30
+ },
31
+ };
32
+
33
+ export default AppState;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * User preferences — singleton, persisted to localStorage.
3
+ * Display preferences that don't need an API.
4
+ * @module store/UserPrefs
5
+ */
6
+
7
+ import { store } from 'hybrids';
8
+
9
+ /**
10
+ * @typedef {Object} UserPrefs
11
+ * @property {'board'|'list'} defaultView - Preferred task view mode
12
+ * @property {boolean} compactMode - Dense UI toggle
13
+ */
14
+
15
+ /** @type {import('hybrids').Model<UserPrefs>} */
16
+ const UserPrefs = {
17
+ defaultView: 'list',
18
+ compactMode: false,
19
+ [store.connect]: {
20
+ get: () => {
21
+ const raw = localStorage.getItem('userPrefs');
22
+ return raw ? JSON.parse(raw) : {};
23
+ },
24
+ set: (id, values) => {
25
+ localStorage.setItem('userPrefs', JSON.stringify(values));
26
+ return values;
27
+ },
28
+ },
29
+ };
30
+
31
+ export default UserPrefs;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Realtime sync via Server-Sent Events.
3
+ * Debounces rapid updates to avoid clearing stores mid-render.
4
+ * @module utils/realtimeSync
5
+ */
6
+
7
+ import { store } from 'hybrids';
8
+
9
+ /**
10
+ * Connect to the SSE endpoint and clear store caches on entity updates.
11
+ * Debounces clears — multiple events within 300ms trigger one clear.
12
+ * @param {string} url - SSE endpoint, e.g. '/api/events'
13
+ * @param {Record<string, import('hybrids').Model<any>>} modelMap
14
+ * @returns {() => void} Disconnect function
15
+ */
16
+ export function connectRealtime(url, modelMap) {
17
+ const source = new EventSource(url);
18
+ /** @type {Record<string, ReturnType<typeof setTimeout>>} */
19
+ const timers = {};
20
+
21
+ source.addEventListener('open', () => {
22
+ console.log('[SSE] Connected to', url);
23
+ });
24
+
25
+ source.addEventListener('update', (event) => {
26
+ const { type, action } = JSON.parse(event.data);
27
+ const Model = modelMap[type];
28
+ if (!Model) return;
29
+
30
+ // Debounce: batch rapid events (e.g. reorder sends N PUTs)
31
+ clearTimeout(timers[type]);
32
+ timers[type] = setTimeout(() => {
33
+ console.log(`[SSE] ${type} ${action} — clearing store cache`);
34
+ try {
35
+ store.clear([Model]);
36
+ } catch {
37
+ /* list may not exist */
38
+ }
39
+ try {
40
+ store.clear(Model);
41
+ } catch {
42
+ /* singular may not exist */
43
+ }
44
+ }, 300);
45
+ });
46
+
47
+ source.addEventListener('error', () => {
48
+ console.log('[SSE] Connection lost, reconnecting in 5s...');
49
+ source.close();
50
+ setTimeout(() => connectRealtime(url, modelMap), 5000);
51
+ });
52
+
53
+ return () => source.close();
54
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "semi": true,
3
+ "singleQuote": true,
4
+ "trailingComma": "all",
5
+ "tabWidth": 2,
6
+ "printWidth": 100,
7
+ "arrowParens": "always"
8
+ }
@@ -0,0 +1,64 @@
1
+ import prettier from 'eslint-config-prettier';
2
+ import jsdoc from 'eslint-plugin-jsdoc';
3
+
4
+ export default [
5
+ {
6
+ files: ['**/*.js'],
7
+ plugins: { jsdoc },
8
+ languageOptions: {
9
+ ecmaVersion: 2024,
10
+ sourceType: 'module',
11
+ globals: {
12
+ console: 'readonly',
13
+ document: 'readonly',
14
+ window: 'readonly',
15
+ HTMLElement: 'readonly',
16
+ CustomEvent: 'readonly',
17
+ EventSource: 'readonly',
18
+ localStorage: 'readonly',
19
+ fetch: 'readonly',
20
+ setTimeout: 'readonly',
21
+ setInterval: 'readonly',
22
+ clearInterval: 'readonly',
23
+ requestAnimationFrame: 'readonly',
24
+ process: 'readonly',
25
+ URL: 'readonly',
26
+ URLSearchParams: 'readonly',
27
+ Event: 'readonly',
28
+ globalThis: 'readonly',
29
+ },
30
+ },
31
+ rules: {
32
+ semi: ['error', 'always'],
33
+ indent: ['error', 2, { SwitchCase: 1 }],
34
+ 'no-var': 'error',
35
+ 'prefer-const': 'error',
36
+ eqeqeq: ['error', 'always'],
37
+ 'no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
38
+ 'no-console': 'off',
39
+
40
+ // JSDoc enforcement
41
+ 'jsdoc/require-jsdoc': [
42
+ 'warn',
43
+ {
44
+ require: { FunctionDeclaration: true },
45
+ checkConstructors: false,
46
+ },
47
+ ],
48
+ 'jsdoc/require-param-type': 'warn',
49
+ 'jsdoc/require-returns-type': 'warn',
50
+ 'jsdoc/valid-types': 'warn',
51
+ },
52
+ },
53
+ {
54
+ files: ['**/*.test.js'],
55
+ rules: {
56
+ 'jsdoc/require-jsdoc': 'off',
57
+ 'no-unused-vars': 'off',
58
+ },
59
+ },
60
+ {
61
+ ignores: ['node_modules/', 'public/vendor/'],
62
+ },
63
+ prettier,
64
+ ];
@@ -0,0 +1,24 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ES2022",
5
+ "moduleResolution": "bundler",
6
+ "checkJs": true,
7
+ "allowJs": true,
8
+ "noEmit": true,
9
+ "strict": false,
10
+ "skipLibCheck": true,
11
+ "paths": {
12
+ "hybrids": ["../node_modules/hybrids/types/index.d.ts"]
13
+ }
14
+ },
15
+ "include": [
16
+ "../src/**/*.js",
17
+ "../scripts/**/*.js"
18
+ ],
19
+ "exclude": [
20
+ "../node_modules",
21
+ "../public/vendor",
22
+ "../**/*.test.js"
23
+ ]
24
+ }
@@ -0,0 +1,8 @@
1
+ import { playwrightLauncher } from '@web/test-runner-playwright';
2
+
3
+ export default {
4
+ files: 'src/components/**/*.test.js',
5
+ nodeResolve: true,
6
+ rootDir: '..',
7
+ browsers: [playwrightLauncher({ product: 'chromium' })],
8
+ };
@@ -0,0 +1,9 @@
1
+ # Spec enforcement defaults
2
+ SPEC_CODE_MAX_LINES=150
3
+ SPEC_DOCS_MAX_LINES=500
4
+ SPEC_CODE_EXTENSIONS=.js,.css
5
+ SPEC_DOCS_EXTENSIONS=.md
6
+ SPEC_IGNORE_DIRS=node_modules,public/vendor,.git,.configs
7
+
8
+ # Server
9
+ PORT={{port}}
@@ -0,0 +1,42 @@
1
+ ---
2
+ name: Bug Report
3
+ about: Something isn't working as expected
4
+ title: "[Bug] "
5
+ labels: bug
6
+ ---
7
+
8
+ ## Describe the bug
9
+
10
+ <!-- Clear description of what's wrong -->
11
+
12
+ ## Steps to reproduce
13
+
14
+ 1.
15
+ 2.
16
+ 3.
17
+
18
+ ## Expected behavior
19
+
20
+ <!-- What should happen -->
21
+
22
+ ## Actual behavior
23
+
24
+ <!-- What actually happens -->
25
+
26
+ ## Environment
27
+
28
+ - Browser:
29
+ - OS:
30
+ - Node version:
31
+
32
+ ## Console errors
33
+
34
+ ```
35
+ <!-- Paste any console errors here -->
36
+ ```
37
+
38
+ ## Spec check output
39
+
40
+ ```
41
+ <!-- Paste output of `npm run spec all` if relevant -->
42
+ ```
@@ -0,0 +1,30 @@
1
+ ---
2
+ name: Feature Request
3
+ about: Suggest a new feature or improvement
4
+ title: "[Feature] "
5
+ labels: enhancement
6
+ ---
7
+
8
+ ## What
9
+
10
+ <!-- What feature or improvement do you want? -->
11
+
12
+ ## Why
13
+
14
+ <!-- What problem does it solve? What use case does it enable? -->
15
+
16
+ ## Proposed approach
17
+
18
+ <!-- How would you implement it? Which files/components would change? -->
19
+
20
+ ## Spec impact
21
+
22
+ <!-- Would this require changes to any spec docs in docs/? -->
23
+
24
+ - [ ] No spec changes needed
25
+ - [ ] New patterns to document
26
+ - [ ] Existing spec needs correction
27
+
28
+ ## Alternatives considered
29
+
30
+ <!-- What other approaches did you think about? -->
@@ -0,0 +1,26 @@
1
+ ---
2
+ name: Spec Correction
3
+ about: The spec says one thing but implementation reveals another
4
+ title: "[Spec] "
5
+ labels: spec
6
+ ---
7
+
8
+ ## Which spec doc?
9
+
10
+ <!-- e.g. COMPONENT_PATTERNS.md, STATE_AND_ROUTING.md -->
11
+
12
+ ## What the spec says
13
+
14
+ <!-- Quote the relevant section -->
15
+
16
+ ## What actually happens
17
+
18
+ <!-- What did you discover during implementation? -->
19
+
20
+ ## Proposed correction
21
+
22
+ <!-- How should the spec be updated? -->
23
+
24
+ ## Code evidence
25
+
26
+ <!-- Link to the file/line that demonstrates the correct behavior -->
@@ -0,0 +1,51 @@
1
+ ## What
2
+
3
+ <!-- Brief description of what this PR does -->
4
+
5
+ ## Why
6
+
7
+ <!-- What problem does it solve or what feature does it add? -->
8
+
9
+ ## Changes
10
+
11
+ <!-- List the key changes. Group by category if helpful. -->
12
+
13
+ ### Added
14
+ -
15
+
16
+ ### Changed
17
+ -
18
+
19
+ ### Fixed
20
+ -
21
+
22
+ ## Spec Compliance
23
+
24
+ <!-- Paste the output of `npm run spec all` -->
25
+
26
+ ```
27
+ npm run spec all
28
+ ```
29
+
30
+ ## Tests
31
+
32
+ <!-- What tests were added or updated? -->
33
+
34
+ - [ ] New tests cover the changes
35
+ - [ ] All existing tests still pass
36
+ - [ ] `npm run spec all` passes (7/7)
37
+
38
+ ## Spec Updates
39
+
40
+ <!-- Did any spec docs need updating? If so, which ones and why? -->
41
+
42
+ - [ ] No spec changes needed
43
+ - [ ] Updated: <!-- list docs -->
44
+
45
+ ## Session Retrospective
46
+
47
+ <!-- Answer briefly — helps future contributors understand the journey -->
48
+
49
+ 1. **Patterns discovered:**
50
+ 2. **Unexpected breakage:**
51
+ 3. **What would you test differently?**
@@ -0,0 +1,46 @@
1
+ name: Spec Check
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ spec:
11
+ name: Full Spec Compliance
12
+ runs-on: ubuntu-latest
13
+
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+
17
+ - uses: actions/setup-node@v4
18
+ with:
19
+ node-version: 20
20
+ cache: npm
21
+
22
+ - run: npm ci
23
+
24
+ - name: Install Playwright Chromium
25
+ run: npx playwright install chromium --with-deps
26
+
27
+ - name: Code line counts (≤150)
28
+ run: node scripts/spec.js code
29
+
30
+ - name: Doc line counts (≤500)
31
+ run: node scripts/spec.js docs
32
+
33
+ - name: ESLint
34
+ run: npx eslint --config .configs/eslint.config.js .
35
+
36
+ - name: Prettier
37
+ run: npx prettier --config .configs/.prettierrc --check src scripts server.js tests
38
+
39
+ - name: JSDoc types (tsc --checkJs)
40
+ run: npx tsc --project .configs/jsconfig.json
41
+
42
+ - name: Node tests
43
+ run: node --test tests/*.test.js src/utils/*.test.js src/store/*.test.js
44
+
45
+ - name: Browser tests
46
+ run: npx web-test-runner --config .configs/web-test-runner.config.js