@nan0web/ui 1.12.2 → 3.1.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 +18 -355
- package/package.json +36 -22
- package/src/Component/index.js +1 -5
- package/src/Model/Element.js +183 -0
- package/src/Model/index.js +2 -2
- package/src/Theme/AppTheme.js +19 -0
- package/src/Theme/CustomTheme.js +32 -0
- package/src/Theme/DarkLightTheme.js +34 -0
- package/src/Theme/Theme.js +25 -0
- package/src/Theme/atoms/Avatar.js +20 -0
- package/src/Theme/atoms/Badge.js +28 -0
- package/src/Theme/atoms/Button.js +88 -0
- package/src/Theme/atoms/Checkbox.js +26 -0
- package/src/Theme/atoms/Input.js +28 -0
- package/src/Theme/atoms/Radio.js +26 -0
- package/src/Theme/atoms/Select.js +16 -0
- package/src/Theme/atoms/TextArea.js +17 -0
- package/src/Theme/atoms/Typography.js +26 -0
- package/src/Theme/atoms/index.js +11 -0
- package/src/Theme/createTheme.js +22 -0
- package/src/Theme/index.js +20 -0
- package/src/Theme/molecules/Card.js +24 -0
- package/src/Theme/molecules/index.js +3 -0
- package/src/Theme/organisms/Modal.js +24 -0
- package/src/Theme/organisms/index.js +3 -0
- package/src/Theme/presets/HighContrastTheme.js +65 -0
- package/src/Theme/presets/NightTheme.js +66 -0
- package/src/Theme/presets/index.js +4 -0
- package/src/Theme/tokens.js +115 -0
- package/src/core/InputAdapter.js +1 -2
- package/src/core/Intent.js +22 -8
- package/src/core/Message/Message.js +3 -0
- package/src/core/OutputAdapter.js +9 -13
- package/src/core/index.js +7 -4
- package/src/core/resolvePositionalArgs.js +51 -0
- package/src/domain/Content.js +5 -5
- package/src/domain/Document.js +1 -1
- package/src/domain/HeroModel.js +1 -1
- package/src/domain/ModelAsApp.js +310 -20
- package/src/domain/ModelAsApp.story.js +117 -0
- package/src/domain/app/GalleryCommand.js +9 -8
- package/src/domain/app/{GalleryRenderIntent.js → GalleryRenderCommand.js} +20 -20
- package/src/domain/app/IntentAuditor.js +53 -0
- package/src/domain/app/JsIntentAuditor.js +145 -0
- package/src/domain/app/PyIntentAuditor.js +144 -0
- package/src/domain/app/SnapshotAuditor.js +82 -86
- package/src/domain/app/SnapshotRunner.js +1 -1
- package/src/domain/app/UIApp.js +12 -21
- package/src/domain/components/ShellModel.js +2 -2
- package/src/index.js +38 -10
- package/src/inspect.js +4 -0
- package/src/testing/SnapshotRunner.js +2 -1
- package/src/utils/format.js +21 -0
- package/src/utils/processI18n.js +27 -0
- package/src/utils/resolveContext.js +79 -0
- package/types/Component/index.d.ts +1 -5
- package/types/Model/Element.d.ts +87 -0
- package/types/Model/index.d.ts +2 -2
- package/types/Theme/AppTheme.d.ts +14 -0
- package/types/Theme/CustomTheme.d.ts +21 -0
- package/types/Theme/DarkLightTheme.d.ts +16 -0
- package/types/Theme/Theme.d.ts +18 -0
- package/types/Theme/atoms/Avatar.d.ts +14 -0
- package/types/Theme/atoms/Badge.d.ts +22 -0
- package/types/Theme/atoms/Button.d.ts +144 -0
- package/types/Theme/atoms/Checkbox.d.ts +20 -0
- package/types/Theme/atoms/Input.d.ts +22 -0
- package/types/Theme/atoms/Radio.d.ts +20 -0
- package/types/Theme/atoms/Select.d.ts +15 -0
- package/types/Theme/atoms/TextArea.d.ts +17 -0
- package/types/Theme/atoms/Typography.d.ts +47 -0
- package/types/Theme/atoms/index.d.ts +10 -0
- package/types/Theme/createTheme.d.ts +7 -0
- package/types/Theme/index.d.ts +10 -0
- package/types/Theme/molecules/Card.d.ts +18 -0
- package/types/Theme/molecules/index.d.ts +2 -0
- package/types/Theme/organisms/Modal.d.ts +18 -0
- package/types/Theme/organisms/index.d.ts +2 -0
- package/types/Theme/presets/HighContrastTheme.d.ts +2 -0
- package/types/Theme/presets/NightTheme.d.ts +2 -0
- package/types/Theme/presets/index.d.ts +3 -0
- package/types/Theme/tokens.d.ts +119 -0
- package/types/core/Intent.d.ts +10 -7
- package/types/core/Message/Message.d.ts +3 -0
- package/types/core/OutputAdapter.d.ts +2 -4
- package/types/core/index.d.ts +5 -2
- package/types/core/resolvePositionalArgs.d.ts +24 -0
- package/types/docs/README.md.d.ts +1 -0
- package/types/domain/Content.d.ts +2 -2
- package/types/domain/Document.d.ts +4 -3
- package/types/domain/FooterModel.d.ts +2 -1
- package/types/domain/HeroModel.d.ts +2 -2
- package/types/domain/ModelAsApp.d.ts +49 -5
- package/types/domain/ModelAsApp.story.d.ts +1 -0
- package/types/domain/app/GalleryCommand.d.ts +6 -37
- package/types/domain/app/GalleryRenderCommand.d.ts +27 -0
- package/types/domain/app/IntentAuditor.d.ts +23 -0
- package/types/domain/app/JsIntentAuditor.d.ts +22 -0
- package/types/domain/app/PyIntentAuditor.d.ts +22 -0
- package/types/domain/app/SnapshotAuditor.d.ts +34 -25
- package/types/domain/app/SnapshotRunner.d.ts +2 -2
- package/types/domain/app/UIApp.d.ts +14 -11
- package/types/domain/components/ShellModel.d.ts +1 -5
- package/types/index.d.ts +10 -10
- package/types/inspect.d.ts +4 -0
- package/types/testing/verifySnapshot.d.ts +1 -1
- package/types/utils/format.d.ts +5 -0
- package/types/utils/processI18n.d.ts +8 -0
- package/types/utils/resolveContext.d.ts +21 -0
- package/src/App/Command/DepsCommand.js +0 -24
- package/src/App/Core/CoreApp.js +0 -125
- package/src/App/Core/UI.js +0 -63
- package/src/App/Core/Widget.js +0 -61
- package/src/App/Core/index.js +0 -11
- package/src/App/Scenario.js +0 -45
- package/src/App/User/Command/Message.js +0 -3
- package/src/App/User/Command/index.js +0 -5
- package/src/App/User/UserApp.js +0 -85
- package/src/App/User/UserUI.js +0 -20
- package/src/App/User/index.js +0 -9
- package/src/App/index.js +0 -14
- package/src/Component/Process/Input.js +0 -63
- package/src/Component/Process/Process.js +0 -24
- package/src/Component/Process/index.js +0 -5
- package/src/Component/Welcome/Input.js +0 -48
- package/src/Component/Welcome/Welcome.js +0 -22
- package/src/Component/Welcome/index.js +0 -5
- package/src/Frame/Frame.js +0 -608
- package/src/Frame/Props.js +0 -96
- package/src/StdIn.js +0 -100
- package/src/StdOut.js +0 -95
- package/src/View/RenderOptions.js +0 -48
- package/src/View/View.js +0 -306
- package/src/core/Message/index.js +0 -6
- package/types/App/Command/DepsCommand.d.ts +0 -14
- package/types/App/Core/CoreApp.d.ts +0 -70
- package/types/App/Core/UI.d.ts +0 -38
- package/types/App/Core/Widget.d.ts +0 -39
- package/types/App/Core/index.d.ts +0 -10
- package/types/App/Scenario.d.ts +0 -26
- package/types/App/User/Command/Message.d.ts +0 -2
- package/types/App/User/Command/index.d.ts +0 -3
- package/types/App/User/UserApp.d.ts +0 -41
- package/types/App/User/UserUI.d.ts +0 -9
- package/types/App/User/index.d.ts +0 -8
- package/types/App/index.d.ts +0 -12
- package/types/Component/Process/Input.d.ts +0 -48
- package/types/Component/Process/Process.d.ts +0 -13
- package/types/Component/Process/index.d.ts +0 -4
- package/types/Component/Welcome/Input.d.ts +0 -34
- package/types/Component/Welcome/Welcome.d.ts +0 -13
- package/types/Component/Welcome/index.d.ts +0 -4
- package/types/Frame/Frame.d.ts +0 -186
- package/types/Frame/Props.d.ts +0 -77
- package/types/StdIn.d.ts +0 -62
- package/types/StdOut.d.ts +0 -52
- package/types/View/RenderOptions.d.ts +0 -29
- package/types/View/View.d.ts +0 -124
- package/types/core/Message/index.d.ts +0 -4
- package/types/domain/app/GalleryRenderIntent.d.ts +0 -31
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { AuditorModel } from '@nan0web/inspect/domain/AuditorModel'
|
|
2
|
+
import { result, show, progress } from '../../core/Intent.js'
|
|
3
|
+
import { IntentAuditor } from './IntentAuditor.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* JsIntentAuditor — Specialized auditor for JS/TS output hygiene.
|
|
7
|
+
*/
|
|
8
|
+
export class JsIntentAuditor extends AuditorModel {
|
|
9
|
+
/** @type {string[]} Directories to ignore during scanning */
|
|
10
|
+
static IGNORE_DIRS = ['node_modules', '.git', '.venv', '.datasets', 'dist', 'build', 'types', 'play', 'test']
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Checks if a directory or file should be ignored.
|
|
14
|
+
* @param {string} name
|
|
15
|
+
* @returns {boolean}
|
|
16
|
+
*/
|
|
17
|
+
static isIgnored(name) {
|
|
18
|
+
return name.startsWith('.') || JsIntentAuditor.IGNORE_DIRS.includes(name)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Run the JS-specific intent audit.
|
|
23
|
+
* @returns {AsyncGenerator<import('@nan0web/ui').Intent, import('@nan0web/ui').ResultIntent, any>}
|
|
24
|
+
*/
|
|
25
|
+
async *run() {
|
|
26
|
+
const { t } = /** @type {any} */ (this)._
|
|
27
|
+
yield show(t(IntentAuditor.UI.starting, { dir: /** @type {any} */ (this).dir }))
|
|
28
|
+
|
|
29
|
+
const fsDb = /** @type {any} */ (this)._.db
|
|
30
|
+
if (!fsDb) {
|
|
31
|
+
yield show(t(IntentAuditor.UI.errorDb), 'error')
|
|
32
|
+
return result({ success: false })
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const files = []
|
|
36
|
+
const targetDir = fsDb.resolveSync(/** @type {any} */ (this).dir)
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
for await (const entry of fsDb.browse(targetDir, { depth: Infinity })) {
|
|
40
|
+
if (JsIntentAuditor.isIgnored(entry.name)) continue
|
|
41
|
+
|
|
42
|
+
if (
|
|
43
|
+
entry.isFile &&
|
|
44
|
+
/\.(js|ts|jsx|tsx)$/.test(entry.name) &&
|
|
45
|
+
!entry.name.endsWith('.test.js') &&
|
|
46
|
+
!entry.name.endsWith('.story.js') &&
|
|
47
|
+
!entry.path.includes('/test/') &&
|
|
48
|
+
!entry.path.includes('/play/')
|
|
49
|
+
) {
|
|
50
|
+
files.push(entry.path)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
} catch (e) {
|
|
54
|
+
// Directory might be missing
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (files.length === 0) {
|
|
58
|
+
yield show(t(IntentAuditor.UI.doneSuccess, {}), 'success')
|
|
59
|
+
return result({ success: true })
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let hasErrors = false
|
|
63
|
+
const allErrors = []
|
|
64
|
+
|
|
65
|
+
for (const file of files) {
|
|
66
|
+
const displayFile = file.startsWith('@app/') ? file.slice(5) : file
|
|
67
|
+
const content = await fsDb.fetch(file)
|
|
68
|
+
const contentString = typeof content === 'string' ? content : JSON.stringify(content)
|
|
69
|
+
|
|
70
|
+
const fileErrors = JsIntentAuditor.inspectFileContent(contentString, t)
|
|
71
|
+
if (fileErrors.length > 0) {
|
|
72
|
+
const errorMessages = fileErrors.join('; ')
|
|
73
|
+
yield show(
|
|
74
|
+
t(IntentAuditor.UI.auditFailed, { file: displayFile, errors: errorMessages }),
|
|
75
|
+
'error',
|
|
76
|
+
)
|
|
77
|
+
allErrors.push(...fileErrors.map((e) => ({ file: displayFile, error: e })))
|
|
78
|
+
hasErrors = true
|
|
79
|
+
} else {
|
|
80
|
+
yield progress(t(IntentAuditor.UI.auditPassed, { file: displayFile }))
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (hasErrors) {
|
|
85
|
+
yield show(t(IntentAuditor.UI.doneErrors, {}), 'error')
|
|
86
|
+
return result({ success: false, errors: allErrors })
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
yield show(t(IntentAuditor.UI.doneSuccess, {}), 'success')
|
|
90
|
+
return result({ success: true })
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Inspects file content for console.* or process.* writes.
|
|
95
|
+
* @param {string} content Content of the file.
|
|
96
|
+
* @param {import('@nan0web/i18n').TFunction} t Translate function.
|
|
97
|
+
* @returns {string[]} List of error messages.
|
|
98
|
+
*/
|
|
99
|
+
static inspectFileContent(content, t) {
|
|
100
|
+
const errors = []
|
|
101
|
+
const lines = content.split('\n')
|
|
102
|
+
|
|
103
|
+
lines.forEach((lineText, index) => {
|
|
104
|
+
const lineNum = index + 1
|
|
105
|
+
const trimmed = lineText.trim()
|
|
106
|
+
|
|
107
|
+
// Skip comments
|
|
108
|
+
if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*')) {
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 1. Thorough console.* statement scan (including table, trace, etc.)
|
|
113
|
+
const consoleMatch = trimmed.match(/console\.(log|error|warn|info|debug|dir|table|trace|assert)\(/)
|
|
114
|
+
if (consoleMatch) {
|
|
115
|
+
const codeBeforeComment = trimmed.split('//')[0]
|
|
116
|
+
if (codeBeforeComment.includes(consoleMatch[0])) {
|
|
117
|
+
errors.push(
|
|
118
|
+
t(IntentAuditor.UI.errorConsoleLeak, {
|
|
119
|
+
line: lineNum,
|
|
120
|
+
match: consoleMatch[0] + '...',
|
|
121
|
+
}),
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 2. process.stdout/stderr write scan
|
|
127
|
+
const processMatch = trimmed.match(/process\.(stdout|stderr)\.write\(/)
|
|
128
|
+
if (processMatch) {
|
|
129
|
+
const codeBeforeComment = trimmed.split('//')[0]
|
|
130
|
+
if (codeBeforeComment.includes(processMatch[0])) {
|
|
131
|
+
errors.push(
|
|
132
|
+
t(IntentAuditor.UI.errorProcessLeak, {
|
|
133
|
+
line: lineNum,
|
|
134
|
+
match: processMatch[0] + '...',
|
|
135
|
+
}),
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
return errors
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export default JsIntentAuditor
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { AuditorModel } from '@nan0web/inspect/domain/AuditorModel'
|
|
2
|
+
import { result, show, progress } from '../../core/Intent.js'
|
|
3
|
+
import { IntentAuditor } from './IntentAuditor.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* PyIntentAuditor — Specialized auditor for Python output hygiene.
|
|
7
|
+
*/
|
|
8
|
+
export class PyIntentAuditor extends AuditorModel {
|
|
9
|
+
/** @type {string[]} Directories to ignore during scanning */
|
|
10
|
+
static IGNORE_DIRS = ['node_modules', '.git', '.venv', '.datasets', 'dist', 'build', 'types', 'play', 'test', 'tests']
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Checks if a directory or file should be ignored.
|
|
14
|
+
* @param {string} name
|
|
15
|
+
* @returns {boolean}
|
|
16
|
+
*/
|
|
17
|
+
static isIgnored(name) {
|
|
18
|
+
return name.startsWith('.') || PyIntentAuditor.IGNORE_DIRS.includes(name)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Run the Python-specific intent audit.
|
|
23
|
+
* @returns {AsyncGenerator<import('@nan0web/ui').Intent, import('@nan0web/ui').ResultIntent, any>}
|
|
24
|
+
*/
|
|
25
|
+
async *run() {
|
|
26
|
+
const { t } = /** @type {any} */ (this)._
|
|
27
|
+
yield show(t(IntentAuditor.UI.starting, { dir: /** @type {any} */ (this).dir }))
|
|
28
|
+
|
|
29
|
+
const fsDb = /** @type {any} */ (this)._.db
|
|
30
|
+
if (!fsDb) {
|
|
31
|
+
yield show(t(IntentAuditor.UI.errorDb), 'error')
|
|
32
|
+
return result({ success: false })
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const files = []
|
|
36
|
+
const targetDir = fsDb.resolveSync(/** @type {any} */ (this).dir)
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
for await (const entry of fsDb.browse(targetDir, { depth: Infinity })) {
|
|
40
|
+
if (PyIntentAuditor.isIgnored(entry.name)) continue
|
|
41
|
+
|
|
42
|
+
if (
|
|
43
|
+
entry.isFile &&
|
|
44
|
+
/\.py$/.test(entry.name) &&
|
|
45
|
+
!entry.name.startsWith('test_') &&
|
|
46
|
+
!entry.name.endsWith('_test.py') &&
|
|
47
|
+
!entry.path.includes('/tests/')
|
|
48
|
+
) {
|
|
49
|
+
files.push(entry.path)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
} catch (e) {
|
|
53
|
+
// Directory might be missing
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (files.length === 0) {
|
|
57
|
+
yield show(t(IntentAuditor.UI.doneSuccess, {}), 'success')
|
|
58
|
+
return result({ success: true })
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let hasErrors = false
|
|
62
|
+
const allErrors = []
|
|
63
|
+
|
|
64
|
+
for (const file of files) {
|
|
65
|
+
const displayFile = file.startsWith('@app/') ? file.slice(5) : file
|
|
66
|
+
const content = await fsDb.fetch(file)
|
|
67
|
+
const contentString = typeof content === 'string' ? content : JSON.stringify(content)
|
|
68
|
+
|
|
69
|
+
const fileErrors = PyIntentAuditor.inspectFileContent(contentString, t)
|
|
70
|
+
if (fileErrors.length > 0) {
|
|
71
|
+
const errorMessages = fileErrors.join('; ')
|
|
72
|
+
yield show(
|
|
73
|
+
t(IntentAuditor.UI.auditFailed, { file: displayFile, errors: errorMessages }),
|
|
74
|
+
'error',
|
|
75
|
+
)
|
|
76
|
+
allErrors.push(...fileErrors.map((e) => ({ file: displayFile, error: e })))
|
|
77
|
+
hasErrors = true
|
|
78
|
+
} else {
|
|
79
|
+
yield progress(t(IntentAuditor.UI.auditPassed, { file: displayFile }))
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (hasErrors) {
|
|
84
|
+
yield show(t(IntentAuditor.UI.doneErrors, {}), 'error')
|
|
85
|
+
return result({ success: false, errors: allErrors })
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
yield show(t(IntentAuditor.UI.doneSuccess, {}), 'success')
|
|
89
|
+
return result({ success: true })
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Inspects Python file content for print or sys.stdout/stderr writes.
|
|
94
|
+
* @param {string} content Content of the file.
|
|
95
|
+
* @param {import('@nan0web/i18n').TFunction} t Translate function.
|
|
96
|
+
* @returns {string[]} List of error messages.
|
|
97
|
+
*/
|
|
98
|
+
static inspectFileContent(content, t) {
|
|
99
|
+
const errors = []
|
|
100
|
+
const lines = content.split('\n')
|
|
101
|
+
|
|
102
|
+
lines.forEach((lineText, index) => {
|
|
103
|
+
const lineNum = index + 1
|
|
104
|
+
const trimmed = lineText.trim()
|
|
105
|
+
|
|
106
|
+
// Python Comment skip
|
|
107
|
+
if (trimmed.startsWith('#')) {
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 1. Python print statement check
|
|
112
|
+
const printMatch = trimmed.match(/\bprint\(/)
|
|
113
|
+
if (printMatch) {
|
|
114
|
+
const codeBeforeComment = trimmed.split('#')[0]
|
|
115
|
+
if (codeBeforeComment.includes(printMatch[0])) {
|
|
116
|
+
errors.push(
|
|
117
|
+
t(IntentAuditor.UI.errorPrintLeak, {
|
|
118
|
+
line: lineNum,
|
|
119
|
+
match: 'print(...)',
|
|
120
|
+
}),
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 2. sys.stdout/stderr write check
|
|
126
|
+
const sysMatch = trimmed.match(/\bsys\.(stdout|stderr)\.write\(/)
|
|
127
|
+
if (sysMatch) {
|
|
128
|
+
const codeBeforeComment = trimmed.split('#')[0]
|
|
129
|
+
if (codeBeforeComment.includes(sysMatch[0])) {
|
|
130
|
+
errors.push(
|
|
131
|
+
t(IntentAuditor.UI.errorSysWriteLeak, {
|
|
132
|
+
line: lineNum,
|
|
133
|
+
match: sysMatch[0] + '...',
|
|
134
|
+
}),
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
return errors
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export default PyIntentAuditor
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { NaN0 } from '@nan0web/types'
|
|
2
|
-
import { AuditorModel } from '@nan0web/inspect'
|
|
3
|
-
|
|
2
|
+
import { AuditorModel } from '@nan0web/inspect/domain/AuditorModel'
|
|
3
|
+
|
|
4
|
+
import { result, show, progress } from '../../core/Intent.js'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* SnapshotAuditor — Zero-Hallucination Snapshot Validation (Model-as-Schema v2).
|
|
7
8
|
* Parses snapshots without evaluating the app logic and detects artifacts.
|
|
8
|
-
*
|
|
9
|
-
* @extends {AuditorModel}
|
|
10
9
|
*/
|
|
11
10
|
export class SnapshotAuditor extends AuditorModel {
|
|
12
|
-
static alias = '
|
|
11
|
+
static alias = 'snapshots'
|
|
13
12
|
|
|
14
13
|
static dir = {
|
|
15
14
|
type: 'string',
|
|
@@ -24,7 +23,6 @@ export class SnapshotAuditor extends AuditorModel {
|
|
|
24
23
|
default: 'data',
|
|
25
24
|
}
|
|
26
25
|
|
|
27
|
-
/** @type {Object<string, string>} Messages for UI */
|
|
28
26
|
static UI = {
|
|
29
27
|
title: 'Snapshot Auditor',
|
|
30
28
|
description: 'Validates UI snapshots against hallucinations and localization leaks.',
|
|
@@ -36,6 +34,7 @@ export class SnapshotAuditor extends AuditorModel {
|
|
|
36
34
|
auditPassed: 'Audit passed: {file}',
|
|
37
35
|
auditFailed: 'Audit failed for {file}: {errors}',
|
|
38
36
|
|
|
37
|
+
errorDb: 'Database not provided to auditor',
|
|
39
38
|
errorGlitch: 'Filename "{filename}" has multiple consecutive separators (glitch detected).',
|
|
40
39
|
errorShort: 'Filename "{filename}" is too short.',
|
|
41
40
|
errorSyntax: 'Syntax Error: Failed to parse NaN0 file. {msg}',
|
|
@@ -56,7 +55,24 @@ export class SnapshotAuditor extends AuditorModel {
|
|
|
56
55
|
static ARTIFACTS = ['[object Object]', 'undefined', 'NaN']
|
|
57
56
|
|
|
58
57
|
/** @type {string[]} Words to ignore across all languages */
|
|
59
|
-
static EXEMPT_WORDS = [
|
|
58
|
+
static EXEMPT_WORDS = [
|
|
59
|
+
'true',
|
|
60
|
+
'false',
|
|
61
|
+
'value',
|
|
62
|
+
'max',
|
|
63
|
+
'min',
|
|
64
|
+
'step',
|
|
65
|
+
'open',
|
|
66
|
+
'first',
|
|
67
|
+
'what',
|
|
68
|
+
'how',
|
|
69
|
+
'start',
|
|
70
|
+
'code',
|
|
71
|
+
'successfully',
|
|
72
|
+
'enter',
|
|
73
|
+
'with',
|
|
74
|
+
'system',
|
|
75
|
+
]
|
|
60
76
|
|
|
61
77
|
/** @type {RegExp} Pattern for suspicious filenames */
|
|
62
78
|
static SUSPICIOUS_FILENAME = /__|--/
|
|
@@ -64,14 +80,24 @@ export class SnapshotAuditor extends AuditorModel {
|
|
|
64
80
|
/** @type {number} Minimum filename length */
|
|
65
81
|
static MIN_FILENAME_LENGTH = 3
|
|
66
82
|
|
|
83
|
+
/** @type {string[]} Directories to ignore during scanning */
|
|
84
|
+
static IGNORE_DIRS = ['node_modules', '.git', '.venv', '.datasets', 'dist', 'build', 'types']
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Checks if a directory or file should be ignored.
|
|
88
|
+
* @param {string} name
|
|
89
|
+
* @returns {boolean}
|
|
90
|
+
*/
|
|
91
|
+
static isIgnored(name) {
|
|
92
|
+
return name.startsWith('.') || SnapshotAuditor.IGNORE_DIRS.includes(name)
|
|
93
|
+
}
|
|
94
|
+
|
|
67
95
|
/**
|
|
68
96
|
* @param {Partial<SnapshotAuditor> | Record<string, any>} [data={}]
|
|
69
|
-
* @param {Partial<import('@nan0web/
|
|
97
|
+
* @param {Partial<import('@nan0web/ui').ModelAsAppOptions>} [options={}]
|
|
70
98
|
*/
|
|
71
99
|
constructor(data = {}, options = {}) {
|
|
72
100
|
super(data, options)
|
|
73
|
-
/** @type {import('@nan0web/types').ModelOptions} */
|
|
74
|
-
this.options = options
|
|
75
101
|
/** @type {string} Target directory to audit */ this.dir
|
|
76
102
|
/** @type {string} Directory to scan for dictionaries */ this.data
|
|
77
103
|
}
|
|
@@ -104,106 +130,74 @@ export class SnapshotAuditor extends AuditorModel {
|
|
|
104
130
|
/** @type {Record<string, Set<string>>} */
|
|
105
131
|
const dicts = {}
|
|
106
132
|
|
|
107
|
-
let entries = []
|
|
108
133
|
try {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
} else {
|
|
116
|
-
throw e;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
for (const e of entriesList) entries.push(e)
|
|
120
|
-
} catch (e) {
|
|
121
|
-
return dicts
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
for (const entry of entries) {
|
|
125
|
-
if (entry.stat.isDirectory && entry.name !== '_') {
|
|
126
|
-
const lang = entry.name
|
|
127
|
-
if (!dicts[lang]) dicts[lang] = new Set()
|
|
128
|
-
|
|
129
|
-
const scanLang = async (dirPath) => {
|
|
130
|
-
let files = []
|
|
131
|
-
try {
|
|
132
|
-
const entries = await fsDb.listDir(dirPath)
|
|
133
|
-
for (const f of entries) files.push(f)
|
|
134
|
-
} catch (e) {
|
|
135
|
-
return
|
|
134
|
+
// Find all language directories
|
|
135
|
+
const entries = await fsDb
|
|
136
|
+
.listDir(data)
|
|
137
|
+
.catch(async (e) => {
|
|
138
|
+
if (/** @type {any} */ (e).code === 'ENOENT' && !data.startsWith('../')) {
|
|
139
|
+
return await fsDb.listDir('../' + data)
|
|
136
140
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
141
|
+
throw e
|
|
142
|
+
})
|
|
143
|
+
.catch(() => [])
|
|
144
|
+
|
|
145
|
+
for (const entry of entries) {
|
|
146
|
+
if (entry.stat.isDirectory && entry.name !== '_') {
|
|
147
|
+
const lang = entry.name
|
|
148
|
+
if (!dicts[lang]) dicts[lang] = new Set()
|
|
149
|
+
|
|
150
|
+
// Use browse for deep dictionary scanning
|
|
151
|
+
for await (const f of fsDb.browse(entry.path, { depth: Infinity })) {
|
|
152
|
+
if (SnapshotAuditor.isIgnored(f.name)) continue
|
|
153
|
+
if (f.isFile) {
|
|
142
154
|
try {
|
|
143
|
-
const
|
|
144
|
-
const raw = _fsDb.FS ? await _fsDb.FS.loadTXT(_fsDb.location(f.path), '', true) : await fsDb.fetch(f.path)
|
|
155
|
+
const raw = await fsDb.fetch(f.path)
|
|
145
156
|
SnapshotAuditor.extractWords(raw, dicts[lang])
|
|
146
157
|
} catch (e) {}
|
|
147
158
|
}
|
|
148
159
|
}
|
|
149
160
|
}
|
|
150
|
-
await scanLang(entry.path)
|
|
151
161
|
}
|
|
162
|
+
} catch (e) {
|
|
163
|
+
// Ignore dictionary errors
|
|
152
164
|
}
|
|
153
165
|
return dicts
|
|
154
166
|
}
|
|
155
167
|
|
|
156
168
|
/**
|
|
157
169
|
* Run the snapshot audit inside the target directory.
|
|
158
|
-
* @returns {AsyncGenerator<import('@nan0web/ui').Intent,
|
|
170
|
+
* @returns {AsyncGenerator<import('@nan0web/ui').Intent, import('@nan0web/ui').ResultIntent, any>}
|
|
159
171
|
*/
|
|
160
172
|
async *run() {
|
|
161
|
-
const { t } = this.
|
|
162
|
-
const snapshotsDir = this.dir || '.'
|
|
163
|
-
|
|
164
|
-
yield show(t(SnapshotAuditor.UI.starting, { dir: snapshotsDir }))
|
|
173
|
+
const { t } = /** @type {any} */ (this)._
|
|
165
174
|
|
|
166
|
-
|
|
175
|
+
yield show(t(SnapshotAuditor.UI.starting, { dir: /** @type {any} */ (this).dir }))
|
|
167
176
|
|
|
168
|
-
/** @type {
|
|
169
|
-
let fsDb = this.options.db
|
|
170
|
-
if (fsDb && fsDb.mounts && fsDb.mounts.has('')) {
|
|
171
|
-
fsDb = /** @type {import('@nan0web/db').DB} */ (fsDb.mounts.get(''))
|
|
172
|
-
}
|
|
177
|
+
const fsDb = /** @type {any} */ (this)._.db
|
|
173
178
|
|
|
174
179
|
if (!fsDb) {
|
|
175
|
-
yield show(
|
|
180
|
+
yield show(t(SnapshotAuditor.UI.errorDb), 'error')
|
|
176
181
|
return result({ success: false })
|
|
177
182
|
}
|
|
178
183
|
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
throw e;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
for (const entry of entries) {
|
|
192
|
-
if (entry.stat.isDirectory) {
|
|
193
|
-
await findSnapshots(entry.path)
|
|
194
|
-
} else if (entry.name.endsWith('.nan0') || entry.name.endsWith('.txt')) {
|
|
195
|
-
files.push(entry.path)
|
|
196
|
-
}
|
|
184
|
+
const files = []
|
|
185
|
+
const snapshotsDir = fsDb.resolveSync(/** @type {any} */ (this).dir)
|
|
186
|
+
|
|
187
|
+
// Use robust DB.browse for recursive snapshot detection
|
|
188
|
+
try {
|
|
189
|
+
for await (const entry of fsDb.browse(snapshotsDir, { depth: Infinity })) {
|
|
190
|
+
if (SnapshotAuditor.isIgnored(entry.name)) continue
|
|
191
|
+
if (entry.isFile && (entry.name.endsWith('.nan0') || entry.name.endsWith('.txt'))) {
|
|
192
|
+
files.push(entry.path)
|
|
197
193
|
}
|
|
198
|
-
} catch (e) {
|
|
199
|
-
console.error('Error reading dir:', dir, e)
|
|
200
194
|
}
|
|
195
|
+
} catch (e) {
|
|
196
|
+
// Directory might be missing
|
|
201
197
|
}
|
|
202
198
|
|
|
203
|
-
await findSnapshots(snapshotsDir)
|
|
204
|
-
|
|
205
199
|
if (files.length === 0) {
|
|
206
|
-
yield show(t(SnapshotAuditor.UI.noSnapshots, { dir:
|
|
200
|
+
yield show(t(SnapshotAuditor.UI.noSnapshots, { dir: this.dir }), 'error')
|
|
207
201
|
return result({ success: false })
|
|
208
202
|
}
|
|
209
203
|
|
|
@@ -216,8 +210,7 @@ export class SnapshotAuditor extends AuditorModel {
|
|
|
216
210
|
const locale = segments[segments.indexOf('core') + 1] || 'uk'
|
|
217
211
|
const componentName = segments.pop() || ''
|
|
218
212
|
|
|
219
|
-
const
|
|
220
|
-
const content = _fsDb.FS ? await _fsDb.FS.loadTXT(_fsDb.location(file), '', true) : await fsDb.fetch(file)
|
|
213
|
+
const content = await fsDb.fetch(file)
|
|
221
214
|
const textContent = typeof content === 'string' ? content : JSON.stringify(content)
|
|
222
215
|
|
|
223
216
|
return {
|
|
@@ -231,14 +224,17 @@ export class SnapshotAuditor extends AuditorModel {
|
|
|
231
224
|
let hasErrors = false
|
|
232
225
|
|
|
233
226
|
for (const { file, audit } of results) {
|
|
234
|
-
const displayFile = file.startsWith('
|
|
227
|
+
const displayFile = file.startsWith('@app/') ? file.slice(5) : file
|
|
235
228
|
if (audit.score < 100) {
|
|
236
229
|
const errorMessages = audit.errors.join('; ')
|
|
237
|
-
yield show(
|
|
230
|
+
yield show(
|
|
231
|
+
t(SnapshotAuditor.UI.auditFailed, { file: displayFile, errors: errorMessages }),
|
|
232
|
+
'error',
|
|
233
|
+
)
|
|
238
234
|
allErrors.push(...audit.errors.map((e) => ({ file: displayFile, error: e })))
|
|
239
235
|
hasErrors = true
|
|
240
236
|
} else {
|
|
241
|
-
yield
|
|
237
|
+
yield progress(t(SnapshotAuditor.UI.auditPassed, { file: displayFile }))
|
|
242
238
|
}
|
|
243
239
|
}
|
|
244
240
|
|
|
@@ -36,7 +36,7 @@ export class SnapshotRunner extends Model {
|
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* @param {Partial<SnapshotRunner> | Record<string, any>} [data={}]
|
|
39
|
-
* @param {import('@nan0web/types').ModelOptions} [options={}]
|
|
39
|
+
* @param {Partial<import('@nan0web/types').ModelOptions>} [options={}]
|
|
40
40
|
*/
|
|
41
41
|
constructor(data = {}, options = {}) {
|
|
42
42
|
super(data, options)
|
package/src/domain/app/UIApp.js
CHANGED
|
@@ -5,6 +5,11 @@ import GalleryCommand from './GalleryCommand.js'
|
|
|
5
5
|
import ConfigApp from './ConfigApp.js'
|
|
6
6
|
import { show, result } from '../../core/Intent.js'
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* @property {string[]} _positionals
|
|
10
|
+
* @property {string} command Type of command to run
|
|
11
|
+
* @property {boolean} help Show help message
|
|
12
|
+
*/
|
|
8
13
|
export class UIApp extends ModelAsApp {
|
|
9
14
|
static command = {
|
|
10
15
|
type: 'string',
|
|
@@ -40,39 +45,25 @@ export class UIApp extends ModelAsApp {
|
|
|
40
45
|
|
|
41
46
|
/**
|
|
42
47
|
* @param {Partial<UIApp> | Record<string, any>} [data={}]
|
|
43
|
-
* @param {import('@nan0web/types').ModelOptions} [options={}]
|
|
48
|
+
* @param {Partial<import('@nan0web/types').ModelOptions>} [options={}]
|
|
44
49
|
*/
|
|
45
50
|
constructor(data = {}, options = {}) {
|
|
46
51
|
super(data, options)
|
|
47
|
-
/** @type {string[]} */ this._positionals = []
|
|
48
|
-
/** @type {string} Type of command to run */ this.command
|
|
49
|
-
/** @type {boolean} Show help message */ this.help
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
async *run() {
|
|
53
55
|
const t = this._.t
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return result({})
|
|
57
|
-
}
|
|
56
|
+
const cmd = /** @type {any} */ (this).command
|
|
57
|
+
const help = /** @type {any} */ (this).help
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
[opt.alias, opt.name].includes(this.command),
|
|
61
|
-
)
|
|
59
|
+
if (help) return yield* super.run()
|
|
62
60
|
|
|
63
|
-
if (!
|
|
64
|
-
yield show(t(UIApp.UI.unknownCommand, { command:
|
|
61
|
+
if (!cmd || !(cmd instanceof ModelAsApp)) {
|
|
62
|
+
yield show(t(UIApp.UI.unknownCommand, { command: cmd }), 'error')
|
|
65
63
|
return result({ status: 'error' })
|
|
66
64
|
}
|
|
67
65
|
|
|
68
|
-
|
|
69
|
-
const nextData = resolvePositionalArgs(
|
|
70
|
-
/** @type {any} */ (TargetCommand),
|
|
71
|
-
this._positionals || [],
|
|
72
|
-
this
|
|
73
|
-
)
|
|
74
|
-
const intent = new TargetCommand(nextData, this._)
|
|
75
|
-
return yield* intent.run()
|
|
66
|
+
return yield* cmd.run()
|
|
76
67
|
}
|
|
77
68
|
}
|
|
78
69
|
|
|
@@ -37,7 +37,7 @@ export class ShellModel extends Model {
|
|
|
37
37
|
],
|
|
38
38
|
required: true,
|
|
39
39
|
}
|
|
40
|
-
|
|
40
|
+
|
|
41
41
|
static data = {
|
|
42
42
|
help: 'Data source (DSN)',
|
|
43
43
|
type: 'string',
|
|
@@ -207,7 +207,7 @@ export class ShellModel extends Model {
|
|
|
207
207
|
if (!spawn) return yield log('error', 'Spawn missing')
|
|
208
208
|
|
|
209
209
|
const { existsSync } = await import('node:fs')
|
|
210
|
-
const viteConfig = existsSync('vite.docs.js') ? 'vite.docs.js' :
|
|
210
|
+
const viteConfig = existsSync('vite.docs.js') ? 'vite.docs.js' :
|
|
211
211
|
existsSync('vite.config.js') ? 'vite.config.js' : null
|
|
212
212
|
|
|
213
213
|
if (viteConfig) {
|