mikoshi-construct 0.1.3 → 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.
- package/README.md +16 -4
- package/dist/cli.js +981 -184
- package/package.json +3 -2
- package/templates/ai/claude/_claude/agents/architect.md +7 -11
- package/templates/ai/claude/_claude/agents/harness.md +7 -11
- package/templates/ai/claude/_claude/agents/implementer.md +6 -10
- package/templates/ai/claude/_claude/skills/implement/SKILL.md +28 -7
- package/templates/ai/claude/scripts/construct/implement.workflow.mjs +53 -9
- package/templates/ai/cursor/_cursor/rules/construct.mdc +1 -1
- package/templates/ai/shared/_claude/commands/construct-discover.md +25 -4
- package/templates/presets/monorepo/baseline/eslint.config.mjs.eta +22 -6
- package/templates/presets/monorepo/baseline/scripts/tests/lint/syntax-policy.test.ts.eta +110 -0
- package/templates/presets/node-backend/baseline/eslint.config.mjs +11 -3
- package/templates/presets/node-backend/baseline/scripts/tests/lint/syntax-policy.test.ts +86 -0
- package/templates/presets/node-frontend/baseline/eslint.config.mjs +12 -3
- package/templates/presets/node-frontend/baseline/scripts/tests/lint/syntax-policy.test.ts +78 -0
- package/templates/presets/monorepo/sample/scripts/tests/lint/syntax-policy.test.ts.eta +0 -36
- package/templates/presets/node-backend/sample/scripts/tests/lint/syntax-policy.test.ts +0 -31
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { ESLint } from 'eslint'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { REPO_ROOT } from '../../composition/files.js'
|
|
4
|
+
|
|
5
|
+
const SAMPLES = {
|
|
6
|
+
plainModule: 'export function open(element: HTMLElement): void {\n element.dataset.state = \'open\'\n}\n',
|
|
7
|
+
classListCall: 'export function open(element: HTMLElement): void {\n element.classList.add(\'is-open\')\n}\n',
|
|
8
|
+
classListComputed: 'export function open(element: HTMLElement): void {\n element.classList[\'add\'](\'is-open\')\n}\n',
|
|
9
|
+
classListOnComputedElement: 'export function open(element: HTMLElement): void {\n element[\'classList\'].add(\'is-open\')\n}\n',
|
|
10
|
+
classListBound: 'export function open(element: HTMLElement): void {\n const classes = element.classList\n classes.add(\'is-open\')\n}\n',
|
|
11
|
+
inlineStyleWrite: 'export function open(element: HTMLElement): void {\n element.style.display = \'block\'\n}\n',
|
|
12
|
+
inlineStyleWriteComputed: 'export function open(element: HTMLElement): void {\n element[\'style\'].display = \'block\'\n}\n',
|
|
13
|
+
inlineStyleProperty: 'export function open(element: HTMLElement): void {\n element.style.setProperty(\'--open\', \'1\')\n}\n',
|
|
14
|
+
inlineStylePropertyComputed: 'export function open(element: HTMLElement): void {\n element.style[\'setProperty\'](\'--open\', \'1\')\n}\n',
|
|
15
|
+
inlineStyleBound: 'export function open(element: HTMLElement): void {\n const style = element.style\n style.display = \'block\'\n}\n',
|
|
16
|
+
} as const
|
|
17
|
+
|
|
18
|
+
type SampleName = keyof typeof SAMPLES
|
|
19
|
+
|
|
20
|
+
const CLASS_LIST_FORMS: SampleName[] = ['classListCall', 'classListComputed', 'classListOnComputedElement', 'classListBound']
|
|
21
|
+
const INLINE_STYLE_FORMS: SampleName[] = ['inlineStyleWrite', 'inlineStyleWriteComputed', 'inlineStyleProperty', 'inlineStylePropertyComputed', 'inlineStyleBound']
|
|
22
|
+
|
|
23
|
+
const ROLES: Array<{ role: string, file: string, reported: SampleName[], exempt: SampleName[] }> = [
|
|
24
|
+
{
|
|
25
|
+
role: 'an application module',
|
|
26
|
+
file: 'src/app.ts',
|
|
27
|
+
reported: [...CLASS_LIST_FORMS, ...INLINE_STYLE_FORMS],
|
|
28
|
+
exempt: [],
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
role: 'the entry point',
|
|
32
|
+
file: 'src/main.ts',
|
|
33
|
+
reported: [...CLASS_LIST_FORMS, ...INLINE_STYLE_FORMS],
|
|
34
|
+
exempt: [],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
role: 'a test',
|
|
38
|
+
file: 'tests/app.test.ts',
|
|
39
|
+
reported: [],
|
|
40
|
+
exempt: [...CLASS_LIST_FORMS, ...INLINE_STYLE_FORMS],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
role: 'a repository script',
|
|
44
|
+
file: 'scripts/composition/check.ts',
|
|
45
|
+
reported: [],
|
|
46
|
+
exempt: [...CLASS_LIST_FORMS, ...INLINE_STYLE_FORMS],
|
|
47
|
+
},
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
const eslint = new ESLint({
|
|
51
|
+
cwd: REPO_ROOT,
|
|
52
|
+
ruleFilter: ({ ruleId }) => ruleId === 'no-restricted-syntax',
|
|
53
|
+
overrideConfig: { languageOptions: { parserOptions: { projectService: false } } },
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
async function restrictionReports(file: string, sample: SampleName): Promise<string[]> {
|
|
57
|
+
const [result] = await eslint.lintText(SAMPLES[sample], { filePath: file })
|
|
58
|
+
const fatal = result.messages.find(message => message.fatal)
|
|
59
|
+
if (fatal)
|
|
60
|
+
throw new Error(`${file} (${sample}): ${fatal.message}`)
|
|
61
|
+
return result.messages
|
|
62
|
+
.filter(message => message.ruleId === 'no-restricted-syntax')
|
|
63
|
+
.map(message => message.message)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
describe('styling policy by file role', () => {
|
|
67
|
+
for (const { role, file, reported, exempt } of ROLES) {
|
|
68
|
+
it(`reports every restricted form in ${role}`, async () => {
|
|
69
|
+
for (const sample of reported)
|
|
70
|
+
expect(await restrictionReports(file, sample), sample).not.toEqual([])
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it(`reports nothing ${role} may legitimately write`, async () => {
|
|
74
|
+
for (const sample of ['plainModule' as SampleName, ...exempt])
|
|
75
|
+
expect(await restrictionReports(file, sample), sample).toEqual([])
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
})
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { ESLint } from 'eslint'
|
|
2
|
-
import { describe, expect, it } from 'vitest'
|
|
3
|
-
import { REPO_ROOT } from '../../composition/files.js'
|
|
4
|
-
|
|
5
|
-
const PROCESS = 'MemberExpression[object.name="process"]'
|
|
6
|
-
const PROCESS_ENV = 'MemberExpression[object.name="process"][property.name="env"]'
|
|
7
|
-
const RAW_REQUEST_DATA = 'MemberExpression[object.name="req"][property.name=/^(body|query|params)$/]'
|
|
8
|
-
const RAW_SQL = 'CallExpression[callee.object.name="sql"][callee.property.name="raw"]'
|
|
9
|
-
const SHARED_IMPORT = ':matches(ImportDeclaration, ExportNamedDeclaration, ExportAllDeclaration)[source.value=/^{{scope}}\\/shared(\\/|$)/]'
|
|
10
|
-
|
|
11
|
-
const eslint = new ESLint({ cwd: REPO_ROOT })
|
|
12
|
-
|
|
13
|
-
async function restrictedSelectors(file: string): Promise<string[]> {
|
|
14
|
-
const config = await eslint.calculateConfigForFile(file)
|
|
15
|
-
const [, ...restrictions] = config.rules['no-restricted-syntax'] as [unknown, ...Array<{ selector: string }>]
|
|
16
|
-
return restrictions.map(restriction => restriction.selector).sort()
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const ROLES: Array<{ role: string, file: string, selectors: string[] }> = [
|
|
20
|
-
{ role: 'a service', file: 'apps/api/src/things/things.service.ts', selectors: [PROCESS, RAW_REQUEST_DATA, RAW_SQL, SHARED_IMPORT] },
|
|
21
|
-
{ role: 'a controller', file: 'apps/api/src/things/things.controller.ts', selectors: [PROCESS, RAW_SQL, SHARED_IMPORT] },
|
|
22
|
-
{ role: 'a middleware', file: 'apps/api/src/http/error-handler.middleware.ts', selectors: [PROCESS, RAW_SQL, SHARED_IMPORT] },
|
|
23
|
-
{ role: 'the contract types gateway', file: 'apps/api/src/contracts/types.ts', selectors: [PROCESS, RAW_REQUEST_DATA, RAW_SQL] },
|
|
24
|
-
{ role: 'the app config', file: 'apps/api/src/config.ts', selectors: [RAW_SQL, SHARED_IMPORT] },
|
|
25
|
-
{ role: 'the process entry', file: 'apps/api/src/server.ts', selectors: [RAW_SQL, SHARED_IMPORT] },
|
|
26
|
-
{ role: 'a package module', file: 'packages/shared/src/index.ts', selectors: [PROCESS_ENV, RAW_SQL] },
|
|
27
|
-
{ role: 'a package config', file: 'packages/shared/src/shared.config.ts', selectors: [RAW_SQL] },
|
|
28
|
-
]
|
|
29
|
-
|
|
30
|
-
describe('syntax policy by file role', () => {
|
|
31
|
-
for (const { role, file, selectors } of ROLES) {
|
|
32
|
-
it(`keeps every restriction that applies to ${role}`, async () => {
|
|
33
|
-
expect(await restrictedSelectors(file)).toEqual([...selectors].sort())
|
|
34
|
-
})
|
|
35
|
-
}
|
|
36
|
-
})
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { ESLint } from 'eslint'
|
|
2
|
-
import { describe, expect, it } from 'vitest'
|
|
3
|
-
import { REPO_ROOT } from '../../composition/files.js'
|
|
4
|
-
|
|
5
|
-
const PROCESS = 'MemberExpression[object.name="process"]'
|
|
6
|
-
const RAW_REQUEST_DATA = 'MemberExpression[object.name="req"][property.name=/^(body|query|params)$/]'
|
|
7
|
-
const RAW_SQL = 'CallExpression[callee.object.name="sql"][callee.property.name="raw"]'
|
|
8
|
-
|
|
9
|
-
const eslint = new ESLint({ cwd: REPO_ROOT })
|
|
10
|
-
|
|
11
|
-
async function restrictedSelectors(file: string): Promise<string[]> {
|
|
12
|
-
const config = await eslint.calculateConfigForFile(file)
|
|
13
|
-
const [, ...restrictions] = config.rules['no-restricted-syntax'] as [unknown, ...Array<{ selector: string }>]
|
|
14
|
-
return restrictions.map(restriction => restriction.selector).sort()
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const ROLES: Array<{ role: string, file: string, selectors: string[] }> = [
|
|
18
|
-
{ role: 'a service', file: 'src/things/things.service.ts', selectors: [PROCESS, RAW_REQUEST_DATA, RAW_SQL] },
|
|
19
|
-
{ role: 'a controller', file: 'src/things/things.controller.ts', selectors: [PROCESS, RAW_SQL] },
|
|
20
|
-
{ role: 'a middleware', file: 'src/http/error-handler.middleware.ts', selectors: [PROCESS, RAW_SQL] },
|
|
21
|
-
{ role: 'the app config', file: 'src/config.ts', selectors: [RAW_SQL] },
|
|
22
|
-
{ role: 'the process entry', file: 'src/server.ts', selectors: [RAW_SQL] },
|
|
23
|
-
]
|
|
24
|
-
|
|
25
|
-
describe('syntax policy by file role', () => {
|
|
26
|
-
for (const { role, file, selectors } of ROLES) {
|
|
27
|
-
it(`keeps every restriction that applies to ${role}`, async () => {
|
|
28
|
-
expect(await restrictedSelectors(file)).toEqual([...selectors].sort())
|
|
29
|
-
})
|
|
30
|
-
}
|
|
31
|
-
})
|