@longph2102/v-flow 1.5.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/AGENTS.md +265 -0
- package/CHANGELOG.md +318 -0
- package/LICENSE +21 -0
- package/README.md +326 -0
- package/agents/ba-agent.md +437 -0
- package/agents/ba-critic-agent.md +156 -0
- package/agents/ba-to-ptyc-agent.md +112 -0
- package/agents/bugfix-analyst-agent.md +221 -0
- package/agents/constitute-agent.md +155 -0
- package/agents/help-agent.md +168 -0
- package/agents/implement-agent.md +220 -0
- package/agents/import-ba-docs-agent.md +164 -0
- package/agents/master-check-agent.md +228 -0
- package/agents/metrics-agent.md +180 -0
- package/agents/operations-agent.md +123 -0
- package/agents/plan-agent.md +218 -0
- package/agents/prototype-agent.md +191 -0
- package/agents/retrospective-agent.md +196 -0
- package/agents/review-agent.md +210 -0
- package/agents/sprint-agent.md +191 -0
- package/agents/status-agent.md +186 -0
- package/agents/sync-agent.md +201 -0
- package/agents/test-agent.md +166 -0
- package/agents/understand-agent.md +339 -0
- package/cli/commands/check.js +96 -0
- package/cli/commands/dev-quiz.js +107 -0
- package/cli/commands/doctor.js +348 -0
- package/cli/commands/feature.js +259 -0
- package/cli/commands/hooks.js +163 -0
- package/cli/commands/init.js +189 -0
- package/cli/commands/log.js +199 -0
- package/cli/commands/plugin.js +230 -0
- package/cli/commands/score-card.js +203 -0
- package/cli/commands/status.js +269 -0
- package/cli/commands/sync.js +59 -0
- package/cli/commands/upgrade.js +150 -0
- package/cli/commands/validate.js +1259 -0
- package/cli/commands/watch.js +151 -0
- package/cli/index.js +46 -0
- package/cli/lib/ac-test-gate.js +89 -0
- package/cli/lib/activity-log.js +209 -0
- package/cli/lib/cli-error.js +183 -0
- package/cli/lib/constitution-lint.js +561 -0
- package/cli/lib/dev-quiz-grade.js +127 -0
- package/cli/lib/governance.js +78 -0
- package/cli/lib/hook-targets.js +167 -0
- package/cli/lib/i18n.js +375 -0
- package/cli/lib/knowledge-oracle.js +379 -0
- package/cli/lib/logger.js +203 -0
- package/cli/lib/module-card-lint.js +304 -0
- package/cli/lib/module-card-score.js +223 -0
- package/cli/lib/plugins.js +481 -0
- package/cli/lib/scanner.js +692 -0
- package/cli/lib/sync-core.js +232 -0
- package/cli/lib/sync-targets.js +84 -0
- package/cli/lib/templates.js +268 -0
- package/cli/lib/yaml-parser.js +203 -0
- package/commands/v.ba-critic.md +101 -0
- package/commands/v.ba-to-ptyc.md +71 -0
- package/commands/v.bugfix.md +86 -0
- package/commands/v.check.md +131 -0
- package/commands/v.constitute.md +87 -0
- package/commands/v.constitution.md +84 -0
- package/commands/v.fork.md +127 -0
- package/commands/v.help.md +73 -0
- package/commands/v.hotfix.md +200 -0
- package/commands/v.implement.md +92 -0
- package/commands/v.import-ba-docs.md +222 -0
- package/commands/v.metrics.md +74 -0
- package/commands/v.operations.md +70 -0
- package/commands/v.plan.md +78 -0
- package/commands/v.prototype.md +121 -0
- package/commands/v.quickfix.md +169 -0
- package/commands/v.retrospective.md +80 -0
- package/commands/v.review.md +78 -0
- package/commands/v.rewind.md +127 -0
- package/commands/v.specify.md +118 -0
- package/commands/v.sprint.md +75 -0
- package/commands/v.status.md +62 -0
- package/commands/v.sync.md +81 -0
- package/commands/v.test.md +67 -0
- package/commands/v.understand.md +112 -0
- package/package.json +65 -0
- package/skills/_shared/constitution-reader/SKILL.md +109 -0
- package/skills/_shared/constitution-reader/config.json +52 -0
- package/skills/_shared/constitution-reader/examples/good/b1-phase-output.md +48 -0
- package/skills/_shared/constitution-reader/gotchas.md +46 -0
- package/skills/_shared/context-reader/SKILL.md +111 -0
- package/skills/_shared/context-reader/config.json +54 -0
- package/skills/_shared/context-reader/examples/good/legacy-nodejs-output.md +35 -0
- package/skills/_shared/context-reader/gotchas.md +49 -0
- package/skills/_shared/ears-notation/SKILL.md +63 -0
- package/skills/_shared/ears-notation/config.json +55 -0
- package/skills/_shared/ears-notation/examples/good/plan-test-interpretation.md +29 -0
- package/skills/_shared/ears-notation/gotchas.md +43 -0
- package/skills/check/cross-validator/SKILL.md +206 -0
- package/skills/check/cross-validator/config.json +33 -0
- package/skills/check/cross-validator/examples/good/validation-report-pass-with-concerns.md +105 -0
- package/skills/check/cross-validator/gotchas.md +43 -0
- package/skills/implement/constitution-enforcer/SKILL.md +134 -0
- package/skills/implement/constitution-enforcer/config.json +16 -0
- package/skills/implement/constitution-enforcer/examples/bad/vague-report.md +42 -0
- package/skills/implement/constitution-enforcer/examples/good/compliance-report.md +57 -0
- package/skills/implement/constitution-enforcer/gotchas.md +26 -0
- package/skills/implement/constitution-enforcer/scripts/check-constitution.sh +88 -0
- package/skills/implement/no-go-zone-guard/SKILL.md +173 -0
- package/skills/implement/no-go-zone-guard/config.json +28 -0
- package/skills/implement/no-go-zone-guard/examples/good/adapter-workaround.md +46 -0
- package/skills/implement/no-go-zone-guard/gotchas.md +27 -0
- package/skills/implement/no-go-zone-guard/scripts/check-nogo-zones.sh +148 -0
- package/skills/implement/no-go-zone-guard/scripts/nogo-precommit.sh +96 -0
- package/skills/implement/tdd-driver/SKILL.md +159 -0
- package/skills/implement/tdd-driver/config.json +33 -0
- package/skills/implement/tdd-driver/examples/good/tdd-cycle-product-repo.md +81 -0
- package/skills/implement/tdd-driver/gotchas.md +34 -0
- package/skills/metrics/metrics-collector/SKILL.md +133 -0
- package/skills/metrics/metrics-collector/config.json +16 -0
- package/skills/metrics/metrics-collector/examples/bad/incomplete-report.md +48 -0
- package/skills/metrics/metrics-collector/examples/good/full-metrics-report.md +101 -0
- package/skills/metrics/metrics-collector/gotchas.md +26 -0
- package/skills/operations/incident-runbook/SKILL.md +167 -0
- package/skills/operations/incident-runbook/config.json +21 -0
- package/skills/operations/incident-runbook/examples/bad/vague-incident-report.md +48 -0
- package/skills/operations/incident-runbook/examples/good/p1-hotfix-response.md +119 -0
- package/skills/operations/incident-runbook/gotchas.md +26 -0
- package/skills/plan/architecture-designer/SKILL.md +228 -0
- package/skills/plan/architecture-designer/config.json +32 -0
- package/skills/plan/architecture-designer/examples/bad/vague-plan.md +62 -0
- package/skills/plan/architecture-designer/examples/good/expand-contract-migration.md +56 -0
- package/skills/plan/architecture-designer/examples/good/plan-structure.md +58 -0
- package/skills/plan/architecture-designer/gotchas.md +45 -0
- package/skills/plan/task-breakdown/SKILL.md +208 -0
- package/skills/plan/task-breakdown/config.json +26 -0
- package/skills/plan/task-breakdown/examples/bad/vague-tasks.md +77 -0
- package/skills/plan/task-breakdown/examples/good/spike-clarify-tasks.md +66 -0
- package/skills/plan/task-breakdown/examples/good/tasks-login-feature.md +111 -0
- package/skills/plan/task-breakdown/gotchas.md +39 -0
- package/skills/prototype/LOGIC.md +240 -0
- package/skills/prototype/SKILL.md +185 -0
- package/skills/prototype/UI.md +407 -0
- package/skills/prototype/config.json +104 -0
- package/skills/prototype/examples/bad/prototype-notes.md +68 -0
- package/skills/prototype/examples/good/prototype-notes-ui.md +109 -0
- package/skills/prototype/examples/good/prototype-notes.md +67 -0
- package/skills/prototype/gotchas.md +128 -0
- package/skills/prototype/scripts/check-flow-state.ps1 +112 -0
- package/skills/prototype/scripts/check-flow-state.sh +104 -0
- package/skills/prototype/scripts/check-prototype-cleanup.ps1 +124 -0
- package/skills/prototype/scripts/check-prototype-cleanup.sh +109 -0
- package/skills/prototype/scripts/check-prototype-notes.ps1 +107 -0
- package/skills/prototype/scripts/check-prototype-notes.sh +102 -0
- package/skills/review/adversarial-reviewer/SKILL.md +137 -0
- package/skills/review/adversarial-reviewer/config.json +32 -0
- package/skills/review/adversarial-reviewer/examples/good/review-report-template.md +56 -0
- package/skills/review/adversarial-reviewer/gotchas.md +46 -0
- package/skills/review/adversarial-reviewer/scripts/quick-security-scan.sh +52 -0
- package/skills/specify/ba-bpmn-doc-gen/SKILL.md +108 -0
- package/skills/specify/ba-bpmn-doc-gen/reference/reference-bpmn-generation.md +528 -0
- package/skills/specify/ba-bpmn-doc-gen/reference/reference-drawio-flowchart.md +466 -0
- package/skills/specify/ba-critic/SKILL.md +172 -0
- package/skills/specify/ba-critic/config.json +32 -0
- package/skills/specify/ba-critic/examples/good/critic-report-round1.md +51 -0
- package/skills/specify/ba-critic/gotchas.md +40 -0
- package/skills/specify/ba-critic/scripts/check-spec-quality.sh +72 -0
- package/skills/specify/ba-doc-generator/SKILL.md +102 -0
- package/skills/specify/ba-doc-generator/references/template-clevel.md +84 -0
- package/skills/specify/ba-doc-generator/references/template-compliance.md +83 -0
- package/skills/specify/ba-doc-generator/references/template-dev.md +138 -0
- package/skills/specify/ba-doc-generator/references/template-partner.md +167 -0
- package/skills/specify/ba-doc-generator/references/template-pm.md +92 -0
- package/skills/specify/ba-doc-generator/references/template-review.md +114 -0
- package/skills/specify/ba-doc-generator/references/template-tester.md +108 -0
- package/skills/specify/ba-doc-generator/references/template-user.md +98 -0
- package/skills/specify/bugfix-analyst/SKILL.md +296 -0
- package/skills/specify/bugfix-analyst/config.json +41 -0
- package/skills/specify/bugfix-analyst/examples/bad/common-mistakes.md +71 -0
- package/skills/specify/bugfix-analyst/examples/good/email-validation-bugfix.md +53 -0
- package/skills/specify/bugfix-analyst/gotchas.md +51 -0
- package/skills/specify/ears-writer/SKILL.md +129 -0
- package/skills/specify/ears-writer/config.json +20 -0
- package/skills/specify/ears-writer/examples/bad/common-mistakes.md +17 -0
- package/skills/specify/ears-writer/examples/good/login-requirements.md +41 -0
- package/skills/specify/ears-writer/gotchas.md +43 -0
- package/skills/specify/ears-writer/scripts/check-ears-compliance.sh +51 -0
- package/skills/test/test-case-generator/SKILL.md +161 -0
- package/skills/test/test-case-generator/config.json +33 -0
- package/skills/test/test-case-generator/examples/good/test-cases-login.md +104 -0
- package/skills/test/test-case-generator/gotchas.md +43 -0
- package/skills/understand/ba-docs-scanner/SKILL.md +239 -0
- package/skills/understand/ba-docs-scanner/config.json +47 -0
- package/skills/understand/ba-docs-scanner/examples/good/work-order-br-extract.md +28 -0
- package/skills/understand/ba-docs-scanner/gotchas.md +44 -0
- package/skills/understand/ba-docs-scanner/merge-rules.md +47 -0
- package/skills/understand/codebase-scanner/SKILL.md +260 -0
- package/skills/understand/codebase-scanner/config.json +56 -0
- package/skills/understand/codebase-scanner/examples/good/menu-module-output.md +44 -0
- package/skills/understand/codebase-scanner/gotchas.md +42 -0
- package/skills/understand/codebase-scanner/scripts/scan-project-structure.sh +64 -0
- package/templates/DESIGN.md +456 -0
- package/templates/agent-command-template.yaml +240 -0
- package/templates/agent-config-template.md +170 -0
- package/templates/agent-definition-template.md +145 -0
- package/templates/agent-metrics-template.md +150 -0
- package/templates/api-contract-template.md +72 -0
- package/templates/bugfix-report-template.md +195 -0
- package/templates/bugfix-spec-template.md +134 -0
- package/templates/code-review-report-template.md +119 -0
- package/templates/constitution-template.md +234 -0
- package/templates/context-template.md +94 -0
- package/templates/data-model-template.md +95 -0
- package/templates/decision-log-template.md +92 -0
- package/templates/flow-state-template.yaml +208 -0
- package/templates/github/workflows/v-flow-validate.yml +30 -0
- package/templates/knowledge/adr-template.md +70 -0
- package/templates/knowledge/api-contract-template.md +140 -0
- package/templates/knowledge/domain-glossary.md +29 -0
- package/templates/knowledge/golden-tests-readme.md +115 -0
- package/templates/knowledge/lessons-learned.md +41 -0
- package/templates/knowledge/patterns.md +103 -0
- package/templates/module-card/SKILL.md +85 -0
- package/templates/module-card/api-specs.md +96 -0
- package/templates/module-card/business-quiz.md +119 -0
- package/templates/module-card/cross-service.md +125 -0
- package/templates/module-card/db.md +85 -0
- package/templates/module-card/dev-quiz.md +62 -0
- package/templates/module-card/permissions.md +83 -0
- package/templates/module-card/state-diagram.md +64 -0
- package/templates/module-card/tech-context.md +90 -0
- package/templates/module-card/ui-flows.md +91 -0
- package/templates/module-card/use-cases.md +142 -0
- package/templates/module-template.yaml +161 -0
- package/templates/operations-report-template.md +108 -0
- package/templates/plan-template.md +308 -0
- package/templates/prototype-notes-template.md +116 -0
- package/templates/ptyc/PTYC.template.docx +0 -0
- package/templates/ptyc/ptyc.meta.example.yaml +44 -0
- package/templates/retrospective-report-template.md +136 -0
- package/templates/security-review-template.md +84 -0
- package/templates/session-template.md +167 -0
- package/templates/spec-review-log-template.md +75 -0
- package/templates/spec-template.md +229 -0
- package/templates/sprint-status-template.md +101 -0
- package/templates/tasks-template.md +275 -0
- package/templates/test-cases-template.md +124 -0
- package/templates/ux-checklist-template.md +79 -0
- package/templates/validation-report-template.md +125 -0
- package/templates/vflow-config-template.yaml +22 -0
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constitution Lint — Automated Constitution Enforcement
|
|
3
|
+
*
|
|
4
|
+
* Parses [LINT:xxx] tags from constitution.md and validates
|
|
5
|
+
* source code / project structure against extracted rules.
|
|
6
|
+
*
|
|
7
|
+
* Rule tag format:
|
|
8
|
+
* - [LINT:naming] → file/variable naming rules
|
|
9
|
+
* - [LINT:import] → import/dependency rules
|
|
10
|
+
* - [LINT:structure] → folder/file structure rules
|
|
11
|
+
* - [LINT:error] → error handling rules
|
|
12
|
+
* - [LINT:test] → testing requirements
|
|
13
|
+
* - [LINT:security] → security rules
|
|
14
|
+
* - [LINT:doc] → documentation rules
|
|
15
|
+
* - [LINT:arch] → architecture/layering (forbidden cross-layer imports)
|
|
16
|
+
* - [LINT:xxx:critical] → critical severity (blocks CI)
|
|
17
|
+
*
|
|
18
|
+
* Backward-compatible: constitutions without [LINT:xxx] tags
|
|
19
|
+
* return "0 rules found, skip" — no behavior change.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const fs = require('fs-extra');
|
|
24
|
+
|
|
25
|
+
// ════════════════════════════════════════════════════════════
|
|
26
|
+
// RULE CATEGORIES
|
|
27
|
+
// ════════════════════════════════════════════════════════════
|
|
28
|
+
|
|
29
|
+
const VALID_CATEGORIES = [
|
|
30
|
+
'naming',
|
|
31
|
+
'import',
|
|
32
|
+
'structure',
|
|
33
|
+
'error',
|
|
34
|
+
'test',
|
|
35
|
+
'security',
|
|
36
|
+
'doc',
|
|
37
|
+
'style',
|
|
38
|
+
'api',
|
|
39
|
+
'arch',
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const SEVERITY = {
|
|
43
|
+
WARNING: 'warning',
|
|
44
|
+
ERROR: 'error',
|
|
45
|
+
CRITICAL: 'critical',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// ════════════════════════════════════════════════════════════
|
|
49
|
+
// PARSER — Extract [LINT:xxx] rules from constitution.md
|
|
50
|
+
// ════════════════════════════════════════════════════════════
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Parse constitution.md content and extract lintable rules.
|
|
54
|
+
*
|
|
55
|
+
* Each line containing [LINT:category] becomes a rule.
|
|
56
|
+
* Optional :critical suffix sets severity to CRITICAL.
|
|
57
|
+
*
|
|
58
|
+
* @param {string} content — constitution.md file content
|
|
59
|
+
* @returns {{ rules: Array<{id, category, severity, text, section, lineNumber}>, parseErrors: string[] }}
|
|
60
|
+
*/
|
|
61
|
+
function parseConstitution(content) {
|
|
62
|
+
const rules = [];
|
|
63
|
+
const parseErrors = [];
|
|
64
|
+
const lines = content.split('\n');
|
|
65
|
+
|
|
66
|
+
let currentSection = 'Unknown';
|
|
67
|
+
let ruleIndex = 0;
|
|
68
|
+
|
|
69
|
+
for (let i = 0; i < lines.length; i++) {
|
|
70
|
+
const line = lines[i];
|
|
71
|
+
const lineNumber = i + 1;
|
|
72
|
+
|
|
73
|
+
// Track current section heading
|
|
74
|
+
const headingMatch = line.match(/^##\s+(.+)/);
|
|
75
|
+
if (headingMatch) {
|
|
76
|
+
currentSection = headingMatch[1].trim();
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Look for [LINT:xxx] tags
|
|
81
|
+
const lintMatch = line.match(/\[LINT:([a-z]+)(?::([a-z]+))?\]/i);
|
|
82
|
+
if (!lintMatch) continue;
|
|
83
|
+
|
|
84
|
+
const category = lintMatch[1].toLowerCase();
|
|
85
|
+
const modifier = lintMatch[2] ? lintMatch[2].toLowerCase() : null;
|
|
86
|
+
|
|
87
|
+
// Validate category
|
|
88
|
+
if (!VALID_CATEGORIES.includes(category)) {
|
|
89
|
+
parseErrors.push(`Line ${lineNumber}: Unknown LINT category "${category}". Valid: ${VALID_CATEGORIES.join(', ')}`);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Determine severity
|
|
94
|
+
let severity = SEVERITY.ERROR;
|
|
95
|
+
if (modifier === 'critical') {
|
|
96
|
+
severity = SEVERITY.CRITICAL;
|
|
97
|
+
} else if (modifier === 'warn' || modifier === 'warning') {
|
|
98
|
+
severity = SEVERITY.WARNING;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Extract rule text (everything after the [LINT:xxx] tag)
|
|
102
|
+
const ruleText = line
|
|
103
|
+
.replace(/\[LINT:[^\]]+\]/i, '')
|
|
104
|
+
.replace(/^[-*]\s*/, '')
|
|
105
|
+
.trim();
|
|
106
|
+
|
|
107
|
+
if (!ruleText) {
|
|
108
|
+
parseErrors.push(`Line ${lineNumber}: [LINT:${category}] tag found but no rule text`);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
ruleIndex++;
|
|
113
|
+
rules.push({
|
|
114
|
+
id: `CL-${String(ruleIndex).padStart(3, '0')}`,
|
|
115
|
+
category,
|
|
116
|
+
severity,
|
|
117
|
+
text: ruleText,
|
|
118
|
+
section: currentSection,
|
|
119
|
+
lineNumber,
|
|
120
|
+
raw: line.trim(),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return { rules, parseErrors };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ════════════════════════════════════════════════════════════
|
|
128
|
+
// RULE ENGINE — Check rules against source files
|
|
129
|
+
// ════════════════════════════════════════════════════════════
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Built-in rule checkers by category.
|
|
133
|
+
* Each checker receives (rule, projectRoot, options) and returns violations[].
|
|
134
|
+
*/
|
|
135
|
+
const RULE_CHECKERS = {
|
|
136
|
+
/**
|
|
137
|
+
* Naming convention rules.
|
|
138
|
+
* Checks filenames against patterns extracted from rule text.
|
|
139
|
+
*/
|
|
140
|
+
naming: async (rule, projectRoot, options) => {
|
|
141
|
+
const violations = [];
|
|
142
|
+
const srcDir = options.srcDir || path.join(projectRoot, 'src');
|
|
143
|
+
if (!fs.existsSync(srcDir)) return violations;
|
|
144
|
+
|
|
145
|
+
const text = rule.text.toLowerCase();
|
|
146
|
+
|
|
147
|
+
// Detect kebab-case requirement
|
|
148
|
+
if (text.includes('kebab-case') || text.includes('kebab case')) {
|
|
149
|
+
const files = await walkFiles(srcDir, options.extensions || ['.js', '.ts', '.jsx', '.tsx', '.vue']);
|
|
150
|
+
for (const file of files) {
|
|
151
|
+
const basename = path.basename(file, path.extname(file));
|
|
152
|
+
// Skip index files and dotfiles
|
|
153
|
+
if (basename === 'index' || basename.startsWith('.')) continue;
|
|
154
|
+
// Check if NOT kebab-case (allow lowercase-only too)
|
|
155
|
+
if (basename !== basename.toLowerCase() || /[A-Z_]/.test(basename)) {
|
|
156
|
+
// Allow UPPER_CASE constants files
|
|
157
|
+
if (basename === basename.toUpperCase()) continue;
|
|
158
|
+
violations.push({
|
|
159
|
+
ruleId: rule.id,
|
|
160
|
+
severity: rule.severity,
|
|
161
|
+
file: path.relative(projectRoot, file),
|
|
162
|
+
message: `Filename "${path.basename(file)}" is not kebab-case`,
|
|
163
|
+
rule: rule.text,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Detect PascalCase requirement for components
|
|
170
|
+
if (text.includes('pascalcase') || text.includes('pascal case') || text.includes('pascal-case')) {
|
|
171
|
+
const files = await walkFiles(srcDir, options.extensions || ['.jsx', '.tsx', '.vue']);
|
|
172
|
+
for (const file of files) {
|
|
173
|
+
const basename = path.basename(file, path.extname(file));
|
|
174
|
+
if (basename === 'index') continue;
|
|
175
|
+
if (!/^[A-Z]/.test(basename)) {
|
|
176
|
+
violations.push({
|
|
177
|
+
ruleId: rule.id,
|
|
178
|
+
severity: rule.severity,
|
|
179
|
+
file: path.relative(projectRoot, file),
|
|
180
|
+
message: `Component file "${path.basename(file)}" should be PascalCase`,
|
|
181
|
+
rule: rule.text,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return violations;
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Import/dependency rules.
|
|
192
|
+
* Checks for forbidden imports.
|
|
193
|
+
*/
|
|
194
|
+
import: async (rule, projectRoot, options) => {
|
|
195
|
+
const violations = [];
|
|
196
|
+
const srcDir = options.srcDir || path.join(projectRoot, 'src');
|
|
197
|
+
if (!fs.existsSync(srcDir)) return violations;
|
|
198
|
+
|
|
199
|
+
const text = rule.text.toLowerCase();
|
|
200
|
+
|
|
201
|
+
// Detect forbidden import patterns
|
|
202
|
+
const forbiddenMatch = rule.text.match(/(?:không|don't|never|cấm|forbidden|prohibit).*(?:import|require).*[`"']([^`"']+)[`"']/i);
|
|
203
|
+
if (forbiddenMatch) {
|
|
204
|
+
const forbiddenModule = forbiddenMatch[1];
|
|
205
|
+
const files = await walkFiles(srcDir, ['.js', '.ts', '.jsx', '.tsx']);
|
|
206
|
+
for (const file of files) {
|
|
207
|
+
const content = await fs.readFile(file, 'utf8');
|
|
208
|
+
if (content.includes(forbiddenModule)) {
|
|
209
|
+
const lineNum = findLineNumber(content, forbiddenModule);
|
|
210
|
+
violations.push({
|
|
211
|
+
ruleId: rule.id,
|
|
212
|
+
severity: rule.severity,
|
|
213
|
+
file: path.relative(projectRoot, file),
|
|
214
|
+
line: lineNum,
|
|
215
|
+
message: `Forbidden import/require: "${forbiddenModule}"`,
|
|
216
|
+
rule: rule.text,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return violations;
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Structure rules.
|
|
227
|
+
* Checks folder/file existence.
|
|
228
|
+
*/
|
|
229
|
+
structure: async (rule, projectRoot, _options) => {
|
|
230
|
+
const violations = [];
|
|
231
|
+
const text = rule.text;
|
|
232
|
+
|
|
233
|
+
// Check for required directory patterns
|
|
234
|
+
const dirMatch = text.match(/(?:must have|phải có|bắt buộc có).*(?:folder|directory|thư mục).*[`"']([^`"']+)[`"']/i);
|
|
235
|
+
if (dirMatch) {
|
|
236
|
+
const requiredDir = dirMatch[1];
|
|
237
|
+
const fullPath = path.join(projectRoot, requiredDir);
|
|
238
|
+
if (!fs.existsSync(fullPath)) {
|
|
239
|
+
violations.push({
|
|
240
|
+
ruleId: rule.id,
|
|
241
|
+
severity: rule.severity,
|
|
242
|
+
file: requiredDir,
|
|
243
|
+
message: `Required directory "${requiredDir}" not found`,
|
|
244
|
+
rule: rule.text,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Check for required file patterns
|
|
250
|
+
const fileMatch = text.match(/(?:must have|phải có|bắt buộc có).*(?:file).*[`"']([^`"']+)[`"']/i);
|
|
251
|
+
if (fileMatch) {
|
|
252
|
+
const requiredFile = fileMatch[1];
|
|
253
|
+
const fullPath = path.join(projectRoot, requiredFile);
|
|
254
|
+
if (!fs.existsSync(fullPath)) {
|
|
255
|
+
violations.push({
|
|
256
|
+
ruleId: rule.id,
|
|
257
|
+
severity: rule.severity,
|
|
258
|
+
file: requiredFile,
|
|
259
|
+
message: `Required file "${requiredFile}" not found`,
|
|
260
|
+
rule: rule.text,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return violations;
|
|
266
|
+
},
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Error handling rules.
|
|
270
|
+
* Checks for patterns like unhandled async, raw string throws.
|
|
271
|
+
*/
|
|
272
|
+
error: async (rule, projectRoot, options) => {
|
|
273
|
+
const violations = [];
|
|
274
|
+
const srcDir = options.srcDir || path.join(projectRoot, 'src');
|
|
275
|
+
if (!fs.existsSync(srcDir)) return violations;
|
|
276
|
+
|
|
277
|
+
const text = rule.text.toLowerCase();
|
|
278
|
+
|
|
279
|
+
// Check for raw string throws
|
|
280
|
+
if (text.includes('throw') && (text.includes('string') || text.includes('raw') || text.includes('error class'))) {
|
|
281
|
+
const files = await walkFiles(srcDir, ['.js', '.ts']);
|
|
282
|
+
for (const file of files) {
|
|
283
|
+
const content = await fs.readFile(file, 'utf8');
|
|
284
|
+
const rawThrows = content.match(/throw\s+['"`]/g);
|
|
285
|
+
if (rawThrows) {
|
|
286
|
+
violations.push({
|
|
287
|
+
ruleId: rule.id,
|
|
288
|
+
severity: rule.severity,
|
|
289
|
+
file: path.relative(projectRoot, file),
|
|
290
|
+
message: `Found ${rawThrows.length} raw string throw(s) — use Error class instead`,
|
|
291
|
+
rule: rule.text,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return violations;
|
|
298
|
+
},
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Test rules.
|
|
302
|
+
* Checks for test file existence patterns.
|
|
303
|
+
*/
|
|
304
|
+
test: async (rule, projectRoot, options) => {
|
|
305
|
+
const violations = [];
|
|
306
|
+
const srcDir = options.srcDir || path.join(projectRoot, 'src');
|
|
307
|
+
const testDir = options.testDir || path.join(projectRoot, 'tests');
|
|
308
|
+
if (!fs.existsSync(srcDir) || !fs.existsSync(testDir)) return violations;
|
|
309
|
+
|
|
310
|
+
const text = rule.text.toLowerCase();
|
|
311
|
+
|
|
312
|
+
// Check for "every module must have test" type rules
|
|
313
|
+
if (text.includes('test') && (text.includes('every') || text.includes('mỗi') || text.includes('tất cả'))) {
|
|
314
|
+
const sourceFiles = await walkFiles(srcDir, ['.js', '.ts']);
|
|
315
|
+
const testFiles = await walkFiles(testDir, ['.test.js', '.test.ts', '.spec.js', '.spec.ts']);
|
|
316
|
+
const testBasenames = testFiles.map(f => path.basename(f).replace(/\.(test|spec)\.(js|ts)$/, ''));
|
|
317
|
+
|
|
318
|
+
for (const srcFile of sourceFiles) {
|
|
319
|
+
const basename = path.basename(srcFile, path.extname(srcFile));
|
|
320
|
+
if (basename === 'index') continue;
|
|
321
|
+
if (!testBasenames.includes(basename)) {
|
|
322
|
+
violations.push({
|
|
323
|
+
ruleId: rule.id,
|
|
324
|
+
severity: SEVERITY.WARNING, // Always warn, not error
|
|
325
|
+
file: path.relative(projectRoot, srcFile),
|
|
326
|
+
message: `No test file found for "${basename}"`,
|
|
327
|
+
rule: rule.text,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return violations;
|
|
334
|
+
},
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Security rules — basic pattern detection.
|
|
338
|
+
*/
|
|
339
|
+
security: async (rule, projectRoot, options) => {
|
|
340
|
+
const violations = [];
|
|
341
|
+
const srcDir = options.srcDir || path.join(projectRoot, 'src');
|
|
342
|
+
if (!fs.existsSync(srcDir)) return violations;
|
|
343
|
+
|
|
344
|
+
const text = rule.text.toLowerCase();
|
|
345
|
+
|
|
346
|
+
// Check for hardcoded secrets pattern
|
|
347
|
+
if (text.includes('hardcod') || text.includes('secret') || text.includes('password') || text.includes('api key')) {
|
|
348
|
+
const files = await walkFiles(srcDir, ['.js', '.ts', '.jsx', '.tsx']);
|
|
349
|
+
const patterns = [
|
|
350
|
+
/(?:password|secret|api_key|apikey|token)\s*[:=]\s*['"][^'"]{8,}['"]/gi,
|
|
351
|
+
];
|
|
352
|
+
for (const file of files) {
|
|
353
|
+
const content = await fs.readFile(file, 'utf8');
|
|
354
|
+
for (const pattern of patterns) {
|
|
355
|
+
pattern.lastIndex = 0;
|
|
356
|
+
const matches = content.match(pattern);
|
|
357
|
+
if (matches) {
|
|
358
|
+
violations.push({
|
|
359
|
+
ruleId: rule.id,
|
|
360
|
+
severity: SEVERITY.CRITICAL,
|
|
361
|
+
file: path.relative(projectRoot, file),
|
|
362
|
+
message: `Possible hardcoded secret found (${matches.length} occurrence(s))`,
|
|
363
|
+
rule: rule.text,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return violations;
|
|
371
|
+
},
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Documentation rules.
|
|
375
|
+
*/
|
|
376
|
+
doc: async (rule, projectRoot, options) => {
|
|
377
|
+
const violations = [];
|
|
378
|
+
const text = rule.text.toLowerCase();
|
|
379
|
+
|
|
380
|
+
// Check for README requirement
|
|
381
|
+
if (text.includes('readme')) {
|
|
382
|
+
const readmePath = path.join(projectRoot, 'README.md');
|
|
383
|
+
if (!fs.existsSync(readmePath)) {
|
|
384
|
+
violations.push({
|
|
385
|
+
ruleId: rule.id,
|
|
386
|
+
severity: rule.severity,
|
|
387
|
+
file: 'README.md',
|
|
388
|
+
message: 'README.md not found at project root',
|
|
389
|
+
rule: rule.text,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return violations;
|
|
395
|
+
},
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Architecture rules — layering / import-boundary enforcement.
|
|
399
|
+
*
|
|
400
|
+
* Detects rules of the form "files under '<A>' must not import from '<B>'"
|
|
401
|
+
* (two path tokens + a prohibition word), walks files under A, and flags any
|
|
402
|
+
* import/require whose specifier resolves into B. This is the mechanical
|
|
403
|
+
* "enforce architecture, don't just document it" gate.
|
|
404
|
+
*
|
|
405
|
+
* Example: - [LINT:arch] Layer `src/domain` không được import từ `src/infra`
|
|
406
|
+
*/
|
|
407
|
+
arch: async (rule, projectRoot, options) => {
|
|
408
|
+
const violations = [];
|
|
409
|
+
const tokens = [...rule.text.matchAll(/[`'"]([^`'"]+)[`'"]/g)].map(m => m[1]);
|
|
410
|
+
const forbids = /không|not|never|cấm|forbidden|must\s*not|prohibit/i.test(rule.text);
|
|
411
|
+
if (tokens.length < 2 || !forbids) return violations; // need source + target + a prohibition
|
|
412
|
+
|
|
413
|
+
const norm = (p) => p.replace(/^\.\//, '').replace(/\/+$/, '');
|
|
414
|
+
const sourceRel = norm(tokens[0]);
|
|
415
|
+
const targetRel = norm(tokens[1]);
|
|
416
|
+
const sourceAbs = path.resolve(projectRoot, sourceRel);
|
|
417
|
+
const targetAbs = path.resolve(projectRoot, targetRel);
|
|
418
|
+
if (!fs.existsSync(sourceAbs)) return violations;
|
|
419
|
+
|
|
420
|
+
const files = await walkFiles(sourceAbs, options.extensions || ['.js', '.ts', '.jsx', '.tsx', '.vue', '.mjs']);
|
|
421
|
+
const targetBase = path.basename(targetRel);
|
|
422
|
+
|
|
423
|
+
for (const file of files) {
|
|
424
|
+
const content = await fs.readFile(file, 'utf8');
|
|
425
|
+
const lines = content.split('\n');
|
|
426
|
+
for (let i = 0; i < lines.length; i++) {
|
|
427
|
+
const m = lines[i].match(/(?:from|require\s*\(|import)\s*['"]([^'"]+)['"]/);
|
|
428
|
+
if (!m) continue;
|
|
429
|
+
const spec = m[1];
|
|
430
|
+
let hit;
|
|
431
|
+
if (spec.startsWith('.')) {
|
|
432
|
+
const resolved = path.resolve(path.dirname(file), spec);
|
|
433
|
+
hit = resolved === targetAbs || resolved.startsWith(targetAbs + path.sep);
|
|
434
|
+
} else {
|
|
435
|
+
// bare/alias import (e.g. '@/infra/db', 'src/infra/db')
|
|
436
|
+
hit = spec.includes(targetRel) || spec.includes(`/${targetBase}/`) || spec.endsWith(`/${targetBase}`);
|
|
437
|
+
}
|
|
438
|
+
if (hit) {
|
|
439
|
+
violations.push({
|
|
440
|
+
ruleId: rule.id,
|
|
441
|
+
severity: rule.severity,
|
|
442
|
+
file: path.relative(projectRoot, file),
|
|
443
|
+
line: i + 1,
|
|
444
|
+
message: `Layering violation: import "${spec}" — vùng "${targetRel}" bị cấm cho layer "${sourceRel}"`,
|
|
445
|
+
rule: rule.text,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return violations;
|
|
451
|
+
},
|
|
452
|
+
|
|
453
|
+
// Fallback for categories without dedicated checkers
|
|
454
|
+
style: async () => [],
|
|
455
|
+
api: async () => [],
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
// ════════════════════════════════════════════════════════════
|
|
459
|
+
// LINT RUNNER
|
|
460
|
+
// ════════════════════════════════════════════════════════════
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Run all lint rules from a parsed constitution against the project.
|
|
464
|
+
*
|
|
465
|
+
* @param {Array} rules — Parsed rules from parseConstitution()
|
|
466
|
+
* @param {string} projectRoot — Project root directory
|
|
467
|
+
* @param {object} options — { srcDir, testDir, extensions, verbose }
|
|
468
|
+
* @returns {{ violations: Array, stats: { total, errors, warnings, critical, passed } }}
|
|
469
|
+
*/
|
|
470
|
+
async function runConstitutionLint(rules, projectRoot, options = {}) {
|
|
471
|
+
const violations = [];
|
|
472
|
+
|
|
473
|
+
for (const rule of rules) {
|
|
474
|
+
const checker = RULE_CHECKERS[rule.category];
|
|
475
|
+
if (!checker) continue;
|
|
476
|
+
|
|
477
|
+
try {
|
|
478
|
+
const ruleViolations = await checker(rule, projectRoot, options);
|
|
479
|
+
violations.push(...ruleViolations);
|
|
480
|
+
} catch (err) {
|
|
481
|
+
if (options.verbose) {
|
|
482
|
+
violations.push({
|
|
483
|
+
ruleId: rule.id,
|
|
484
|
+
severity: SEVERITY.WARNING,
|
|
485
|
+
file: 'constitution.md',
|
|
486
|
+
message: `Rule check error: ${err.message}`,
|
|
487
|
+
rule: rule.text,
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const stats = {
|
|
494
|
+
total: rules.length,
|
|
495
|
+
errors: violations.filter(v => v.severity === SEVERITY.ERROR).length,
|
|
496
|
+
warnings: violations.filter(v => v.severity === SEVERITY.WARNING).length,
|
|
497
|
+
critical: violations.filter(v => v.severity === SEVERITY.CRITICAL).length,
|
|
498
|
+
passed: rules.length - new Set(violations.map(v => v.ruleId)).size,
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
return { violations, stats };
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// ════════════════════════════════════════════════════════════
|
|
505
|
+
// HELPERS
|
|
506
|
+
// ════════════════════════════════════════════════════════════
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Recursively walk a directory and return files matching extensions.
|
|
510
|
+
*/
|
|
511
|
+
async function walkFiles(dir, extensions) {
|
|
512
|
+
const results = [];
|
|
513
|
+
if (!fs.existsSync(dir)) return results;
|
|
514
|
+
|
|
515
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
516
|
+
for (const entry of entries) {
|
|
517
|
+
const fullPath = path.join(dir, entry.name);
|
|
518
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
|
519
|
+
|
|
520
|
+
if (entry.isDirectory()) {
|
|
521
|
+
const sub = await walkFiles(fullPath, extensions);
|
|
522
|
+
results.push(...sub);
|
|
523
|
+
} else if (extensions.some(ext => entry.name.endsWith(ext))) {
|
|
524
|
+
results.push(fullPath);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return results;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Find the line number of a pattern in content.
|
|
533
|
+
*/
|
|
534
|
+
function findLineNumber(content, pattern) {
|
|
535
|
+
const lines = content.split('\n');
|
|
536
|
+
for (let i = 0; i < lines.length; i++) {
|
|
537
|
+
if (lines[i].includes(pattern)) return i + 1;
|
|
538
|
+
}
|
|
539
|
+
return 0;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Format a violation for display.
|
|
544
|
+
*/
|
|
545
|
+
function formatViolation(violation) {
|
|
546
|
+
const icon = violation.severity === SEVERITY.CRITICAL ? '🔴'
|
|
547
|
+
: violation.severity === SEVERITY.ERROR ? '❌'
|
|
548
|
+
: '⚠️ ';
|
|
549
|
+
const loc = violation.line ? `${violation.file}:${violation.line}` : violation.file;
|
|
550
|
+
return `${icon} [${violation.ruleId}] ${loc} → ${violation.message}`;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
module.exports = {
|
|
554
|
+
VALID_CATEGORIES,
|
|
555
|
+
SEVERITY,
|
|
556
|
+
parseConstitution,
|
|
557
|
+
runConstitutionLint,
|
|
558
|
+
formatViolation,
|
|
559
|
+
walkFiles,
|
|
560
|
+
findLineNumber,
|
|
561
|
+
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev Quiz Grade — machine-graded technical verification (Decision 2: Dev = graph key).
|
|
3
|
+
*
|
|
4
|
+
* Mirror of business-quiz but the answer key is the GRAPH, not a human. Each
|
|
5
|
+
* question declares a `target` symbol + `direction`; ground truth = the symbol's
|
|
6
|
+
* real blast radius from `oracle.impact()`. An answer set is graded by F1 against
|
|
7
|
+
* that ground truth — objective, reproducible, no human grader needed.
|
|
8
|
+
*
|
|
9
|
+
* Two modes (driven by the command):
|
|
10
|
+
* - generate: ground truth only (a graph-derived answer key for a tech lead)
|
|
11
|
+
* - grade: compare an answers file to ground truth → precision/recall/F1
|
|
12
|
+
*
|
|
13
|
+
* Pure: (content, answers, oracle) → result. The oracle is injectable (stub-tested).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Parse `### DQ01: name [target: foo | direction: upstream]` blocks. */
|
|
17
|
+
function parseDevQuestions(content) {
|
|
18
|
+
const out = [];
|
|
19
|
+
const lines = (content || '').split('\n');
|
|
20
|
+
for (const line of lines) {
|
|
21
|
+
const m = line.match(/^###\s+(DQ\d+)\s*:?\s*(.*?)\s*\[([^\]]+)\]/);
|
|
22
|
+
if (!m) continue;
|
|
23
|
+
const meta = {};
|
|
24
|
+
for (const part of m[3].split('|')) {
|
|
25
|
+
const kv = part.split(':');
|
|
26
|
+
if (kv.length >= 2) meta[kv[0].trim().toLowerCase()] = kv.slice(1).join(':').trim();
|
|
27
|
+
}
|
|
28
|
+
if (!meta.target) continue;
|
|
29
|
+
out.push({ id: m[1], name: m[2].trim(), target: meta.target, direction: meta.direction || 'upstream' });
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Parse an answers file: DQ block → list of symbol names after `**Answer**`. */
|
|
35
|
+
function parseAnswers(content) {
|
|
36
|
+
const map = {};
|
|
37
|
+
if (!content) return map;
|
|
38
|
+
const lines = content.split('\n');
|
|
39
|
+
let cur = null;
|
|
40
|
+
let capturing = false;
|
|
41
|
+
for (const line of lines) {
|
|
42
|
+
const h = line.match(/^###\s+(DQ\d+)\b/);
|
|
43
|
+
if (h) {
|
|
44
|
+
cur = h[1];
|
|
45
|
+
map[cur] = map[cur] || [];
|
|
46
|
+
capturing = false;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (!cur) continue;
|
|
50
|
+
if (/^\*\*Answer\*\*/i.test(line)) {
|
|
51
|
+
capturing = true;
|
|
52
|
+
map[cur].push(...tokenize(line.replace(/^\*\*Answer\*\*\s*:?\s*/i, '')));
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (capturing) {
|
|
56
|
+
if (/^\*\*/.test(line) || /^##/.test(line) || /^---/.test(line)) {
|
|
57
|
+
capturing = false;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
map[cur].push(...tokenize(line));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return map;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Extract identifier-ish tokens from a line (handles backticks, commas, bullets). */
|
|
67
|
+
function tokenize(text) {
|
|
68
|
+
return (text.match(/[A-Za-z_][\w.]*/g) || [])
|
|
69
|
+
.map((s) => s.trim())
|
|
70
|
+
.filter((s) => s.length > 1 && !/^(và|hoặc|the|and|or|none|không)$/i.test(s));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function norm(s) {
|
|
74
|
+
return String(s).trim().toLowerCase();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** F1 of an answer set vs a truth set (both arrays of names). */
|
|
78
|
+
function scoreSets(answer, truth) {
|
|
79
|
+
const a = new Set(answer.map(norm));
|
|
80
|
+
const t = new Set(truth.map(norm));
|
|
81
|
+
if (t.size === 0) return { f1: a.size === 0 ? 1 : 0, tp: 0, precision: a.size === 0 ? 1 : 0, recall: 1 };
|
|
82
|
+
let tp = 0;
|
|
83
|
+
for (const x of a) if (t.has(x)) tp++;
|
|
84
|
+
const precision = a.size === 0 ? 0 : tp / a.size;
|
|
85
|
+
const recall = tp / t.size;
|
|
86
|
+
const f1 = precision + recall === 0 ? 0 : (2 * precision * recall) / (precision + recall);
|
|
87
|
+
return { f1: Math.round(f1 * 100) / 100, tp, precision: Math.round(precision * 100) / 100, recall: Math.round(recall * 100) / 100 };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Build the ground-truth answer key from the graph (no answers needed).
|
|
92
|
+
* @returns {{ questions: Array, verified: boolean }}
|
|
93
|
+
*/
|
|
94
|
+
function generateGroundTruth(quizContent, oracle) {
|
|
95
|
+
const questions = parseDevQuestions(quizContent).map((q) => {
|
|
96
|
+
const imp = oracle.impact(q.target, q.direction);
|
|
97
|
+
return {
|
|
98
|
+
...q,
|
|
99
|
+
found: !!imp.found,
|
|
100
|
+
risk: imp.risk,
|
|
101
|
+
truth: imp.found ? imp.affected : [],
|
|
102
|
+
reason: imp.found ? undefined : imp.reason,
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
return { questions, verified: questions.some((q) => q.found) };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Grade an answers file vs graph ground truth.
|
|
110
|
+
* @returns {{ questions: Array, score: number, max: number, verified: boolean }}
|
|
111
|
+
*/
|
|
112
|
+
function gradeDevQuiz(quizContent, answersContent, oracle) {
|
|
113
|
+
const answers = parseAnswers(answersContent);
|
|
114
|
+
const questions = parseDevQuestions(quizContent).map((q) => {
|
|
115
|
+
const imp = oracle.impact(q.target, q.direction);
|
|
116
|
+
if (!imp.supported) return { ...q, verified: false, reason: 'oracle không hỗ trợ impact' };
|
|
117
|
+
if (!imp.found) return { ...q, verified: false, reason: imp.reason || 'symbol không có trong graph' };
|
|
118
|
+
const ans = answers[q.id] || [];
|
|
119
|
+
const s = scoreSets(ans, imp.affected);
|
|
120
|
+
return { ...q, verified: true, risk: imp.risk, truth: imp.affected, answered: ans, ...s };
|
|
121
|
+
});
|
|
122
|
+
const graded = questions.filter((q) => q.verified);
|
|
123
|
+
const score = graded.length === 0 ? 0 : Math.round((graded.reduce((s, q) => s + q.f1, 0) / graded.length) * 100) / 100;
|
|
124
|
+
return { questions, score, max: 1, verified: graded.length > 0 };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = { parseDevQuestions, parseAnswers, tokenize, scoreSets, generateGroundTruth, gradeDevQuiz };
|