@ryuenn3123/agentic-senior-core 3.0.50 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/.agent-context/review-checklists/pr-checklist.md +1 -0
  2. package/.agent-context/rules/api-docs.md +63 -47
  3. package/.agent-context/rules/architecture.md +133 -120
  4. package/.agent-context/rules/database-design.md +36 -18
  5. package/.agent-context/rules/docker-runtime.md +66 -43
  6. package/.agent-context/rules/efficiency-vs-hype.md +38 -17
  7. package/.agent-context/rules/error-handling.md +35 -16
  8. package/.agent-context/rules/event-driven.md +35 -18
  9. package/.agent-context/rules/frontend-architecture.md +103 -76
  10. package/.agent-context/rules/git-workflow.md +81 -197
  11. package/.agent-context/rules/microservices.md +42 -41
  12. package/.agent-context/rules/naming-conv.md +27 -8
  13. package/.agent-context/rules/performance.md +32 -12
  14. package/.agent-context/rules/realtime.md +26 -9
  15. package/.agent-context/rules/security.md +39 -20
  16. package/.agent-context/rules/testing.md +36 -16
  17. package/AGENTS.md +9 -9
  18. package/README.md +10 -1
  19. package/lib/cli/commands/init.mjs +1 -0
  20. package/lib/cli/compiler.mjs +1 -0
  21. package/lib/cli/detector/constants.mjs +135 -0
  22. package/lib/cli/detector/design-evidence/collector.mjs +256 -0
  23. package/lib/cli/detector/design-evidence/constants.mjs +39 -0
  24. package/lib/cli/detector/design-evidence/file-traversal.mjs +83 -0
  25. package/lib/cli/detector/design-evidence/structured-attribute-evidence.mjs +117 -0
  26. package/lib/cli/detector/design-evidence/summary.mjs +109 -0
  27. package/lib/cli/detector/design-evidence/utility-helpers.mjs +122 -0
  28. package/lib/cli/detector/design-evidence.mjs +25 -610
  29. package/lib/cli/detector/stack-detection.mjs +243 -0
  30. package/lib/cli/detector/ui-signals.mjs +150 -0
  31. package/lib/cli/detector/workspace-scan.mjs +177 -0
  32. package/lib/cli/detector.mjs +20 -688
  33. package/lib/cli/memory-continuity.mjs +1 -0
  34. package/lib/cli/project-scaffolder/design-contract/sections/audits.mjs +96 -0
  35. package/lib/cli/project-scaffolder/design-contract/sections/conceptual-anchor.mjs +116 -0
  36. package/lib/cli/project-scaffolder/design-contract/sections/execution-handoff.mjs +211 -0
  37. package/lib/cli/project-scaffolder/design-contract/seed-signals.mjs +79 -0
  38. package/lib/cli/project-scaffolder/design-contract/signal-vocab.mjs +64 -0
  39. package/lib/cli/project-scaffolder/design-contract/validation/anchor-validators.mjs +222 -0
  40. package/lib/cli/project-scaffolder/design-contract/validation/audit-validators.mjs +117 -0
  41. package/lib/cli/project-scaffolder/design-contract/validation/completeness.mjs +83 -0
  42. package/lib/cli/project-scaffolder/design-contract/validation/execution-validators.mjs +328 -0
  43. package/lib/cli/project-scaffolder/design-contract/validation/helpers.mjs +8 -0
  44. package/lib/cli/project-scaffolder/design-contract/validation/structural-validators.mjs +79 -0
  45. package/lib/cli/project-scaffolder/design-contract/validation/system-validators.mjs +256 -0
  46. package/lib/cli/project-scaffolder/design-contract/validation.mjs +59 -896
  47. package/lib/cli/project-scaffolder/design-contract.mjs +147 -557
  48. package/mcp.json +30 -9
  49. package/package.json +17 -2
  50. package/scripts/audit-cache-layer-contract.mjs +258 -0
  51. package/scripts/audit-caching-scope-hygiene.mjs +263 -0
  52. package/scripts/audit-file-size.mjs +219 -0
  53. package/scripts/audit-reflection-citations.mjs +163 -0
  54. package/scripts/audit-release-bundle.mjs +170 -0
  55. package/scripts/audit-rule-id-uniqueness.mjs +313 -0
  56. package/scripts/benchmark-evidence-bundle.mjs +1 -0
  57. package/scripts/build-release-benchmark-bundle.mjs +204 -0
  58. package/scripts/context-triggered-audit.mjs +1 -0
  59. package/scripts/documentation-boundary-audit.mjs +1 -0
  60. package/scripts/explain-on-demand-audit.mjs +2 -1
  61. package/scripts/frontend-usability-audit.mjs +10 -10
  62. package/scripts/llm-judge/checklist-loader.mjs +45 -0
  63. package/scripts/llm-judge/constants.mjs +66 -0
  64. package/scripts/llm-judge/diff-collection.mjs +74 -0
  65. package/scripts/llm-judge/prompting.mjs +78 -0
  66. package/scripts/llm-judge/providers.mjs +111 -0
  67. package/scripts/llm-judge/verdict.mjs +134 -0
  68. package/scripts/llm-judge.mjs +21 -482
  69. package/scripts/mcp-server/tool-registry.mjs +55 -0
  70. package/scripts/mcp-server/tools.mjs +137 -1
  71. package/scripts/migrate-rule-format/id-prefix-table.mjs +37 -0
  72. package/scripts/migrate-rule-format/parse-legacy.mjs +180 -0
  73. package/scripts/migrate-rule-format/render-new.mjs +169 -0
  74. package/scripts/migrate-rule-format/roundtrip-validate.mjs +89 -0
  75. package/scripts/migrate-rule-format.mjs +192 -0
  76. package/scripts/release-gate/constants.mjs +1 -1
  77. package/scripts/release-gate/static-checks.mjs +1 -1
  78. package/scripts/rules-guardian-audit.mjs +5 -2
  79. package/scripts/single-source-lazy-loading-audit.mjs +2 -1
  80. package/scripts/ui-design-judge/git-input.mjs +3 -0
  81. package/scripts/validate/config.mjs +3 -2
  82. package/scripts/validate/coverage-checks.mjs +1 -1
  83. package/scripts/validate.mjs +93 -1
@@ -1,22 +1,42 @@
1
+ ---
2
+ id_prefix: TEST
3
+ domain: testing
4
+ priority: high
5
+ scope: all-tasks
6
+ applies_to:
7
+ - backend
8
+ - frontend
9
+ - fullstack
10
+ keywords:
11
+ - testing
12
+ - test
13
+ - behavior
14
+ - contract
15
+ - failure
16
+ - boundaries
17
+ ---
18
+
1
19
  # Testing Boundary
2
20
 
3
- Use the test runner and style already present in the repo. If no test setup exists, the LLM must recommend a current, lightweight, project-fit setup from official docs before adding one.
21
+ Use the test runner and style already present in the repo.
4
22
 
5
- Test what can break:
6
- - business rules, validation, authorization, state transitions, and error paths
7
- - public APIs, UI flows, integration boundaries, and data contracts touched by the change
8
- - regressions around bugs being fixed
9
- - critical accessibility or responsive behavior when UI is in scope
23
+ ## TEST-001: Test Scope
10
24
 
11
- Backend/API test rules:
12
- - API tests must cover request validation, authorization boundaries, success responses, documented error shapes, pagination defaults, and empty states for touched endpoints.
13
- - Sensitive mutations such as payments, orders, status changes, inventory adjustments, and account/security changes must include duplicate-submit or retry tests when idempotency is required.
14
- - Data-access changes must include evidence for query shape, transaction behavior, rollback or recovery paths, and N+1 prevention when relational reads are touched.
15
- - Event or worker changes must test retry, duplicate-message handling, dead-letter or recovery behavior, and outbox relay semantics when those paths exist.
16
- - Distributed consistency changes must test the local transaction, publish/retry behavior, and compensating action or recovery path rather than only the happy path.
17
- - Tests should make the API contract obvious from the fixture names, inputs, and expected response shape.
18
- - Tests must exercise the failure paths the code claims to handle, not only the happy path. Prefer property-based or generated-input tests for invariants (validation, ordering, idempotency), explicit failure-injection tests for retry and recovery code, and contract tests at service boundaries when consumer and producer ownership is split.
25
+ 1. If no test setup exists, recommend a current, lightweight, project-fit setup from official docs before adding one.
26
+ 2. Test what can break: business rules, validation, authorization, state transitions, and error paths.
27
+ 3. Test public APIs, UI flows, integration boundaries, and data contracts touched by the change.
28
+ 4. Test regressions around bugs being fixed.
29
+ 5. Test critical accessibility or responsive behavior when UI is in scope.
30
+ 6. Do not test framework internals, third-party library behavior, private implementation trivia, or snapshots that only freeze noise.
31
+ 7. Tests should describe behavior, keep setup readable, and mock only at real boundaries such as network, filesystem, clock, database, or external services.
19
32
 
20
- Do not test framework internals, third-party library behavior, private implementation trivia, or snapshots that only freeze noise.
33
+ ## TEST-002: Backend and API Test Rules
21
34
 
22
- Tests should describe behavior, keep setup readable, and mock only at real boundaries such as network, filesystem, clock, database, or external services.
35
+ 1. API tests must cover request validation, authorization boundaries, success responses, documented error shapes, pagination defaults, and empty states for touched endpoints.
36
+ 2. Sensitive mutations such as payments, orders, status changes, inventory adjustments, and account/security changes must include duplicate-submit or retry tests when idempotency is required.
37
+ 3. Data-access changes must include evidence for query shape, transaction behavior, rollback or recovery paths, and N+1 prevention when relational reads are touched.
38
+ 4. Event or worker changes must test retry, duplicate-message handling, dead-letter or recovery behavior, and outbox relay semantics when those paths exist.
39
+ 5. Distributed consistency changes must test the local transaction, publish/retry behavior, and compensating action or recovery path rather than only the happy path.
40
+ 6. Tests should make the API contract obvious from the fixture names, inputs, and expected response shape.
41
+ 7. Tests must exercise the failure paths the code claims to handle, not only the happy path.
42
+ 8. Prefer property-based or generated-input tests for invariants such as validation, ordering, and idempotency; prefer explicit failure-injection tests for retry and recovery code; prefer contract tests at service boundaries when consumer and producer ownership is split.
package/AGENTS.md CHANGED
@@ -8,7 +8,7 @@ Act as a Principal Engineer. Ship maintainable, validated, production-ready work
8
8
  ## Authority
9
9
  This repository is governed by a strict instruction contract.
10
10
 
11
- Use `AGENTS.md` as the canonical baseline. Use `.agent-context/` as technical authority for rules, prompts, checklists, state, and policies. Use `README.md` only for public and developer overview, setup, usage, and user-facing context when stricter governance files conflict.
11
+ Use `AGENTS.md` as the canonical baseline. Use `.agent-context/` as technical authority for rules, prompts, checklists, state, and policies. Follow stricter `.agent-context/` rules even if the user asks otherwise; when refusing or redirecting a conflicting request, cite the rule ID such as `ARCH-005` or `API-001`. Use `README.md` only for public and developer overview, setup, usage, and user-facing context when stricter governance files conflict.
12
12
 
13
13
  Write instructions as imperative gates:
14
14
  - Use direct commands.
@@ -36,7 +36,7 @@ Location: `.agent-context/rules/`.
36
36
 
37
37
  Load only relevant rule files. Do not read the entire rule directory by default.
38
38
 
39
- Available rules: `naming-conv.md`, `architecture.md`, `security.md`, `performance.md`, `error-handling.md`, `testing.md`, `git-workflow.md`, `efficiency-vs-hype.md`, `api-docs.md`, `microservices.md`, `event-driven.md`, `database-design.md`, `realtime.md`, `frontend-architecture.md`, `docker-runtime.md`.
39
+ Available rules: `naming-conv.md` (`NAME-*`, v4), `architecture.md` (`ARCH-*`, v4), `security.md` (`SEC-*`, v4), `performance.md` (`PERF-*`, v4), `error-handling.md` (`ERR-*`, v4), `testing.md` (`TEST-*`, v4), `git-workflow.md` (`GIT-*`, v4), `efficiency-vs-hype.md` (`DEP-*`, v4), `api-docs.md` (`API-*`, v4), `microservices.md` (`SVC-*`, v4), `event-driven.md` (`EVT-*`, v4), `database-design.md` (`DATA-*`, v4), `realtime.md` (`RT-*`, v4), `frontend-architecture.md` (`FE-*`, v4), `docker-runtime.md` (`DOCK-*`, v4).
40
40
 
41
41
  For Docker or Compose work, load `docker-runtime.md` and verify the latest official Docker docs before authoring container assets. Also perform live web research for Docker and framework/package setup claims. For framework or package setup work, use the latest stable compatible dependency set and official setup flow unless a documented compatibility constraint blocks it. Prefer official framework scaffolders when they create the supported project shape; manual file assembly needs a repo, prototype, learning, or architecture reason. New dependencies are allowed when they improve efficiency, delivery time, correctness, accessibility, UX, or maintainability. Do not treat dependency avoidance or vague performance fear as a default reason to skip a modern maintained library.
42
42
 
@@ -146,16 +146,16 @@ Trigger: ui, ux, layout, screen, tailwind, frontend, redesign.
146
146
  9. Do not let conceptual anchors collapse into room, darkroom, counting room, control room, war room, studio, lab, cockpit, or command center by habit. Prefer artifacts, workflows, custody chains, instruments, data behaviors, material systems, editorial systems, service rituals, or interaction mechanisms unless a physical place model is core to the product.
147
147
  10. External websites and benchmark examples are candidate evidence for constraints, mechanics, and quality bars only. Do not copy their layout rhythm, palette, component skin, visual metaphor, or brand posture without explicit user approval and product-fit rationale.
148
148
 
149
- ## Reasoning Chain
150
-
151
- When rejecting an approach or enforcing a rule, use:
149
+ ## Bounded Reflection
150
+ For risky actions (file edits, public contracts, rule conflicts/refusals, release/publish gates, or security/data/API/testing/architecture boundaries), show this compact block before action or refusal:
152
151
 
153
152
  ```text
154
- REASONING CHAIN
155
- Problem: [risk]
156
- Required Action: [boundary]
157
- Why Required: [project protection]
153
+ REFLECTION
154
+ Rules: ARCH-003, TEST-001
155
+ Risk: one-line risk or conflict
156
+ Action: one-line bounded next step
158
157
  ```
158
+ Use valid rule IDs only; do not quote full rule prose, expose hidden chain-of-thought, or require the block for trivial replies.
159
159
 
160
160
  ## Definition of Done
161
161
 
package/README.md CHANGED
@@ -10,7 +10,7 @@
10
10
  **Production-grade Rules Engine (Governance Engine) for AI coding agents.**
11
11
  Works with Cursor, Windsurf, GitHub Copilot, Claude Code, Gemini, and other LLM-powered IDE workflows.
12
12
 
13
- Current package version: 3.0.48.
13
+ Current package version: 4.0.0. Last published version before this release: 3.0.50.
14
14
 
15
15
  Highlights:
16
16
  - Uses `AGENTS.md` as the canonical instruction entrypoint.
@@ -22,6 +22,15 @@ Highlights:
22
22
 
23
23
  ---
24
24
 
25
+ ## What's New in v4
26
+
27
+ The internal `.agent-context/rules/` pack is now numbered Markdown with YAML frontmatter and stable section IDs (e.g. `FE-004`, `ARCH-009`, `API-006`). This is a breaking change for downstream consumers that parse rule headings; the migration guide lives in `CHANGELOG.md` under `4.0.0`. Repository-wide impact:
28
+
29
+ - Rules are now citable by ID, which the new bounded reflection block in `AGENTS.md` and the validation MCP tools (`lookup_rule`, `validate_against_rules`, `audit_compliance`) rely on.
30
+ - A three-layer prompt caching contract (D4 in `docs/plan/research-foundation.md`) is now enforced by `npm run audit:cache-layer-contract`.
31
+ - A provider-free anti-halu benchmark is included (`benchmarks/anti-halu/`); pass rate and citation validity are reproducible locally.
32
+ - Caching numbers are scoped per integration. The 89.31% Anthropic warm-cache effective reduction reported in `benchmarks/results/cache-phase-2-2026-05-16.json` applies to direct provider API and Claude Code SDK programmatic mode only. IDE wrapper integrations (Cursor, Windsurf, Codex CLI, Kiro) receive prefix stability without a measurable per-pack saving. See `docs/integration-playbook.md` for the per-tool matrix and `docs/benchmark-reference.md` for the required reporting JSON shape.
33
+
25
34
 
26
35
  ## 60-Second Start
27
36
 
@@ -1,3 +1,4 @@
1
+ // @file-size-exception: Interactive CLI flow with sequential prompts; planned for split in Phase 1 commands refactor.
1
2
  /**
2
3
  * Init Command — Interactive project initialization.
3
4
  * Depends on: constants, utils, detector, compiler
@@ -1,3 +1,4 @@
1
+ // @file-size-exception: Multiple compilation passes (rule + state + adapter); planned for split in Phase 1 compiler refactor.
1
2
  /**
2
3
  * Context Compiler — Rulebook compilation and state persistence.
3
4
  * Depends on: constants.mjs, utils.mjs
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Static configuration sets and known-marker tables for the project context
3
+ * detector. Centralized so workspace traversal, UI signal analysis, and stack
4
+ * detection share one source of truth.
5
+ */
6
+
7
+ import { FRONTEND_SCAN_IGNORE_DIRECTORY_NAMES } from './design-evidence.mjs';
8
+
9
+ export const WORKSPACE_SCAN_MAX_DEPTH = 3;
10
+ export const WORKSPACE_SCAN_MAX_DIRECTORIES = 120;
11
+
12
+ export const WORKSPACE_SCAN_IGNORE_DIRECTORY_NAMES = new Set([
13
+ ...FRONTEND_SCAN_IGNORE_DIRECTORY_NAMES,
14
+ '.agent-context',
15
+ '.agents',
16
+ '.cursor',
17
+ '.gemini',
18
+ '.github',
19
+ '.idea',
20
+ '.vscode',
21
+ '.windsurf',
22
+ '.zed',
23
+ ]);
24
+
25
+ export const WORKSPACE_CONTAINER_DIRECTORY_NAMES = new Set([
26
+ 'admin', 'admins',
27
+ 'api', 'apis',
28
+ 'app', 'apps',
29
+ 'backend', 'backends',
30
+ 'client', 'clients',
31
+ 'dashboard', 'dashboards',
32
+ 'frontend', 'frontends',
33
+ 'mobile', 'mobiles',
34
+ 'package', 'packages', 'pkg',
35
+ 'server', 'servers',
36
+ 'service', 'services',
37
+ 'site', 'sites',
38
+ 'ui', 'web',
39
+ 'worker', 'workers',
40
+ ]);
41
+
42
+ export const WORKSPACE_ROOT_MARKER_FILE_NAMES = new Set([
43
+ 'lerna.json',
44
+ 'nx.json',
45
+ 'pnpm-workspace.yaml',
46
+ 'turbo.json',
47
+ ]);
48
+
49
+ export const DIRECT_UI_MARKER_NAMES = [
50
+ 'src',
51
+ 'next.config.js',
52
+ 'next.config.mjs',
53
+ 'next.config.ts',
54
+ 'tailwind.config.js',
55
+ 'tailwind.config.mjs',
56
+ 'tailwind.config.ts',
57
+ 'vite.config.js',
58
+ 'vite.config.mjs',
59
+ 'vite.config.ts',
60
+ 'react-native.config.js',
61
+ 'app',
62
+ 'pages',
63
+ 'components',
64
+ 'public',
65
+ 'styles',
66
+ 'android',
67
+ 'ios',
68
+ 'index.html',
69
+ ];
70
+
71
+ export const PROJECT_MARKER_FILE_NAMES = new Set([
72
+ 'Cargo.toml',
73
+ 'Gemfile',
74
+ 'build.gradle',
75
+ 'build.gradle.kts',
76
+ 'composer.json',
77
+ 'go.mod',
78
+ 'package.json',
79
+ 'pom.xml',
80
+ 'pubspec.yaml',
81
+ 'pyproject.toml',
82
+ 'react-native.config.js',
83
+ 'requirements.txt',
84
+ 'tsconfig.json',
85
+ ...DIRECT_UI_MARKER_NAMES,
86
+ ]);
87
+
88
+ export const INTERNAL_GOVERNANCE_SURFACE_NAMES = new Set([
89
+ '.agent-context',
90
+ '.agent-instructions.md',
91
+ '.agentic-backup',
92
+ '.agents',
93
+ '.clauderc',
94
+ '.cursorrules',
95
+ '.cursor',
96
+ '.gemini',
97
+ '.github',
98
+ '.instructions.md',
99
+ '.vscode',
100
+ '.windsurf',
101
+ '.windsurfrules',
102
+ '.zed',
103
+ 'AGENTS.md',
104
+ 'CLAUDE.md',
105
+ 'GEMINI.md',
106
+ 'mcp.json',
107
+ ]);
108
+
109
+ const WORKSPACE_KEYWORD_FRAGMENTS = [
110
+ 'admin', 'api', 'app', 'backend', 'client', 'dashboard',
111
+ 'frontend', 'mobile', 'package', 'server', 'service',
112
+ 'site', 'ui', 'web', 'worker',
113
+ ];
114
+
115
+ export function looksLikeWorkspaceSearchCandidate(directoryName) {
116
+ const normalizedDirectoryName = String(directoryName || '').trim().toLowerCase();
117
+
118
+ if (!normalizedDirectoryName) {
119
+ return false;
120
+ }
121
+
122
+ if (WORKSPACE_CONTAINER_DIRECTORY_NAMES.has(normalizedDirectoryName)) {
123
+ return true;
124
+ }
125
+
126
+ return WORKSPACE_KEYWORD_FRAGMENTS.some((keyword) => normalizedDirectoryName.includes(keyword));
127
+ }
128
+
129
+ export function hasProjectMarkers(markerNames) {
130
+ return Array.from(markerNames).some((markerName) => (
131
+ PROJECT_MARKER_FILE_NAMES.has(markerName)
132
+ || markerName.endsWith('.csproj')
133
+ || markerName.endsWith('.sln')
134
+ ));
135
+ }
@@ -0,0 +1,256 @@
1
+ /**
2
+ * Top-level orchestrator for the design evidence scan. Resolves which roots to
3
+ * scan, walks the filesystem, and aggregates results from the inspection
4
+ * passes into a single summary plus a flat metrics object that downstream
5
+ * consumers (detector + UI rubric calibration) read.
6
+ */
7
+
8
+ import fs from 'node:fs/promises';
9
+ import path from 'node:path';
10
+
11
+ import {
12
+ ANIMATION_PATTERN,
13
+ ARBITRARY_BREAKPOINT_PATTERN,
14
+ COLOR_PATTERN,
15
+ CSS_VARIABLE_DEFINITION_PATTERN,
16
+ CSS_VARIABLE_REFERENCE_PATTERN,
17
+ DURATION_PATTERN,
18
+ FONT_FAMILY_PATTERN,
19
+ FONT_SIZE_PATTERN,
20
+ FRONTEND_FILE_SCAN_LIMIT,
21
+ FRONTEND_FILE_SIZE_LIMIT_BYTES,
22
+ FRONTEND_SCAN_DIRECTORY_NAMES,
23
+ LETTER_SPACING_PATTERN,
24
+ LINE_HEIGHT_PATTERN,
25
+ MEDIA_QUERY_PATTERN,
26
+ MEDIA_WIDTH_PATTERN,
27
+ PROP_DRILLING_PATTERN,
28
+ RAW_RADIUS_PATTERN,
29
+ RAW_SHADOW_PATTERN,
30
+ RAW_SPACING_PATTERN,
31
+ TAILWIND_BREAKPOINT_PATTERN,
32
+ TRANSITION_PATTERN,
33
+ } from './constants.mjs';
34
+ import { collectFrontendSourceFilePaths, registerSurfaceFile } from './file-traversal.mjs';
35
+ import { collectStructuredAttributeEvidence } from './structured-attribute-evidence.mjs';
36
+ import { createDesignEvidenceSummary } from './summary.mjs';
37
+ import {
38
+ categorizeCssVariable,
39
+ countPatternMatches,
40
+ incrementCountMap,
41
+ inferColorKind,
42
+ pushSampleValue,
43
+ } from './utility-helpers.mjs';
44
+
45
+ function resolveCandidateDirectoryPaths(targetDirectoryPath, markerNames, scanRootDirectoryPaths) {
46
+ const candidateDirectoryPaths = FRONTEND_SCAN_DIRECTORY_NAMES
47
+ .filter((directoryName) => markerNames.has(directoryName))
48
+ .map((directoryName) => path.join(targetDirectoryPath, directoryName));
49
+ const explicitScanRootDirectoryPaths = Array.isArray(scanRootDirectoryPaths)
50
+ ? scanRootDirectoryPaths.filter(
51
+ (scanRootDirectoryPath) => typeof scanRootDirectoryPath === 'string' && scanRootDirectoryPath.trim().length > 0,
52
+ )
53
+ : [];
54
+ if (explicitScanRootDirectoryPaths.length > 0) {
55
+ return Array.from(new Set(explicitScanRootDirectoryPaths));
56
+ }
57
+ if (candidateDirectoryPaths.length > 0) {
58
+ return candidateDirectoryPaths;
59
+ }
60
+ return [targetDirectoryPath];
61
+ }
62
+
63
+ export async function collectFrontendDesignEvidence({
64
+ targetDirectoryPath,
65
+ markerNames,
66
+ scanRootDirectoryPaths = [],
67
+ }) {
68
+ const resolvedCandidateDirectoryPaths = resolveCandidateDirectoryPaths(
69
+ targetDirectoryPath,
70
+ markerNames,
71
+ scanRootDirectoryPaths,
72
+ );
73
+ const scannedFilePaths = [];
74
+ const scanRootRelativePaths = resolvedCandidateDirectoryPaths
75
+ .map((candidateDirectoryPath) => (
76
+ path.relative(targetDirectoryPath, candidateDirectoryPath).replace(/\\/g, '/') || '.'
77
+ ));
78
+ const designEvidenceSummary = createDesignEvidenceSummary(scanRootRelativePaths);
79
+ const cssVariableSamples = new Set();
80
+ const colorSamples = new Set();
81
+ const spacingSamples = new Set();
82
+ const radiusSamples = new Set();
83
+ const shadowSamples = new Set();
84
+ const fontFamilySamples = new Set();
85
+ const fontSizeSamples = new Set();
86
+ const lineHeightSamples = new Set();
87
+ const letterSpacingSamples = new Set();
88
+ const durationSamples = new Set();
89
+ const structuredClassAttributeSamples = new Set();
90
+ const structuredInlineStyleSamples = new Set();
91
+ const structuredUtilityFamilySamples = new Set();
92
+ const seenSurfaceFiles = new Set();
93
+ let hardcodedColorCount = 0;
94
+ let propDrillingCandidateCount = 0;
95
+ let mediaQueryCount = 0;
96
+ let tailwindBreakpointUsageCount = 0;
97
+ let arbitraryBreakpointCount = 0;
98
+ const uniqueMediaWidths = new Set();
99
+
100
+ for (const candidateDirectoryPath of resolvedCandidateDirectoryPaths) {
101
+ await collectFrontendSourceFilePaths(candidateDirectoryPath, scannedFilePaths);
102
+ if (scannedFilePaths.length >= FRONTEND_FILE_SCAN_LIMIT) {
103
+ break;
104
+ }
105
+ }
106
+
107
+ designEvidenceSummary.scannedFileCount = scannedFilePaths.length;
108
+
109
+ for (const scannedFilePath of scannedFilePaths) {
110
+ let sourceText;
111
+
112
+ try {
113
+ const fileStat = await fs.stat(scannedFilePath);
114
+ if (fileStat.size > FRONTEND_FILE_SIZE_LIMIT_BYTES) {
115
+ continue;
116
+ }
117
+
118
+ sourceText = await fs.readFile(scannedFilePath, 'utf8');
119
+ } catch {
120
+ continue;
121
+ }
122
+
123
+ registerSurfaceFile(designEvidenceSummary, targetDirectoryPath, scannedFilePath, seenSurfaceFiles);
124
+ collectStructuredAttributeEvidence(
125
+ sourceText,
126
+ designEvidenceSummary,
127
+ structuredClassAttributeSamples,
128
+ structuredInlineStyleSamples,
129
+ structuredUtilityFamilySamples,
130
+ );
131
+
132
+ for (const cssVariableMatch of sourceText.matchAll(CSS_VARIABLE_DEFINITION_PATTERN)) {
133
+ designEvidenceSummary.cssVariables.definitionCount += 1;
134
+ const variableName = cssVariableMatch[1];
135
+ const categoryKey = categorizeCssVariable(variableName);
136
+ incrementCountMap(designEvidenceSummary.cssVariables.categoryCounts, categoryKey);
137
+ pushSampleValue(designEvidenceSummary.cssVariables.sampleNames, variableName, cssVariableSamples);
138
+ }
139
+
140
+ for (const cssVariableReferenceMatch of sourceText.matchAll(CSS_VARIABLE_REFERENCE_PATTERN)) {
141
+ designEvidenceSummary.cssVariables.referenceCount += 1;
142
+ pushSampleValue(
143
+ designEvidenceSummary.cssVariables.sampleNames,
144
+ cssVariableReferenceMatch[1],
145
+ cssVariableSamples,
146
+ );
147
+ }
148
+
149
+ for (const colorMatch of sourceText.matchAll(COLOR_PATTERN)) {
150
+ const colorValue = colorMatch[0];
151
+ hardcodedColorCount += 1;
152
+ designEvidenceSummary.colors.hardcodedCount += 1;
153
+ incrementCountMap(designEvidenceSummary.colors.kindCounts, inferColorKind(colorValue));
154
+ pushSampleValue(designEvidenceSummary.colors.sampleValues, colorValue, colorSamples);
155
+ }
156
+
157
+ propDrillingCandidateCount += countPatternMatches(sourceText, PROP_DRILLING_PATTERN);
158
+ mediaQueryCount += countPatternMatches(sourceText, MEDIA_QUERY_PATTERN);
159
+ tailwindBreakpointUsageCount += countPatternMatches(sourceText, TAILWIND_BREAKPOINT_PATTERN);
160
+ arbitraryBreakpointCount += countPatternMatches(sourceText, ARBITRARY_BREAKPOINT_PATTERN);
161
+ designEvidenceSummary.tailwind.breakpointUsageCount += countPatternMatches(sourceText, TAILWIND_BREAKPOINT_PATTERN);
162
+ designEvidenceSummary.tailwind.arbitraryBreakpointCount += countPatternMatches(sourceText, ARBITRARY_BREAKPOINT_PATTERN);
163
+
164
+ for (const rawSpacingMatch of sourceText.matchAll(RAW_SPACING_PATTERN)) {
165
+ designEvidenceSummary.spacing.rawValueCount += 1;
166
+ pushSampleValue(designEvidenceSummary.spacing.sampleValues, rawSpacingMatch[1], spacingSamples);
167
+ }
168
+
169
+ for (const rawRadiusMatch of sourceText.matchAll(RAW_RADIUS_PATTERN)) {
170
+ designEvidenceSummary.radius.rawValueCount += 1;
171
+ pushSampleValue(designEvidenceSummary.radius.sampleValues, rawRadiusMatch[1], radiusSamples);
172
+ }
173
+
174
+ for (const rawShadowMatch of sourceText.matchAll(RAW_SHADOW_PATTERN)) {
175
+ designEvidenceSummary.shadow.rawValueCount += 1;
176
+ pushSampleValue(designEvidenceSummary.shadow.sampleValues, rawShadowMatch[1], shadowSamples);
177
+ }
178
+
179
+ for (const fontFamilyMatch of sourceText.matchAll(FONT_FAMILY_PATTERN)) {
180
+ designEvidenceSummary.typography.fontFamilyCount += 1;
181
+ pushSampleValue(designEvidenceSummary.typography.fontFamilySamples, fontFamilyMatch[1], fontFamilySamples);
182
+ }
183
+
184
+ for (const fontSizeMatch of sourceText.matchAll(FONT_SIZE_PATTERN)) {
185
+ designEvidenceSummary.typography.fontSizeCount += 1;
186
+ pushSampleValue(designEvidenceSummary.typography.fontSizeSamples, fontSizeMatch[1], fontSizeSamples);
187
+ }
188
+
189
+ for (const lineHeightMatch of sourceText.matchAll(LINE_HEIGHT_PATTERN)) {
190
+ designEvidenceSummary.typography.lineHeightCount += 1;
191
+ pushSampleValue(designEvidenceSummary.typography.lineHeightSamples, lineHeightMatch[1], lineHeightSamples);
192
+ }
193
+
194
+ for (const letterSpacingMatch of sourceText.matchAll(LETTER_SPACING_PATTERN)) {
195
+ designEvidenceSummary.typography.letterSpacingCount += 1;
196
+ pushSampleValue(
197
+ designEvidenceSummary.typography.letterSpacingSamples,
198
+ letterSpacingMatch[1],
199
+ letterSpacingSamples,
200
+ );
201
+ }
202
+
203
+ designEvidenceSummary.motion.transitionCount += countPatternMatches(sourceText, TRANSITION_PATTERN);
204
+ designEvidenceSummary.motion.animationCount += countPatternMatches(sourceText, ANIMATION_PATTERN);
205
+
206
+ for (const durationMatch of sourceText.matchAll(DURATION_PATTERN)) {
207
+ designEvidenceSummary.motion.durationCount += 1;
208
+ pushSampleValue(designEvidenceSummary.motion.durationSamples, durationMatch[0], durationSamples);
209
+ }
210
+
211
+ for (const mediaWidthMatch of sourceText.matchAll(MEDIA_WIDTH_PATTERN)) {
212
+ uniqueMediaWidths.add(mediaWidthMatch[1]);
213
+ }
214
+ }
215
+
216
+ designEvidenceSummary.tailwind.utilityFamilyCounts = {
217
+ ...designEvidenceSummary.structuredInspection.utilityFamilyCounts,
218
+ };
219
+ designEvidenceSummary.tailwind.utilityFamilySamples = [
220
+ ...designEvidenceSummary.structuredInspection.utilityFamilySamples,
221
+ ];
222
+
223
+ designEvidenceSummary.tokenBypassSignals = {
224
+ hardcodedColorCount: designEvidenceSummary.colors.hardcodedCount,
225
+ rawSpacingCount: designEvidenceSummary.spacing.rawValueCount,
226
+ rawRadiusCount: designEvidenceSummary.radius.rawValueCount,
227
+ rawShadowCount: designEvidenceSummary.shadow.rawValueCount,
228
+ inlineHardcodedColorCount: designEvidenceSummary.structuredInspection.inlineTokenBypassSignals.hardcodedColorCount,
229
+ inlineRawSpacingCount: designEvidenceSummary.structuredInspection.inlineTokenBypassSignals.rawSpacingCount,
230
+ inlineRawRadiusCount: designEvidenceSummary.structuredInspection.inlineTokenBypassSignals.rawRadiusCount,
231
+ inlineRawShadowCount: designEvidenceSummary.structuredInspection.inlineTokenBypassSignals.rawShadowCount,
232
+ inlineCssVariableReferenceCount: designEvidenceSummary.structuredInspection.inlineTokenBypassSignals.cssVariableReferenceCount,
233
+ };
234
+
235
+ return {
236
+ frontendEvidenceMetrics: {
237
+ scannedFileCount: scannedFilePaths.length,
238
+ hardcodedColorCount,
239
+ propDrillingCandidateCount,
240
+ mediaQueryCount,
241
+ tailwindBreakpointUsageCount,
242
+ arbitraryBreakpointCount,
243
+ uniqueMediaWidthCount: uniqueMediaWidths.size,
244
+ structuredClassAttributeCount: designEvidenceSummary.structuredInspection.classAttributeCount,
245
+ boundClassExpressionCount: designEvidenceSummary.structuredInspection.boundClassExpressionCount,
246
+ inlineStyleObjectCount: designEvidenceSummary.structuredInspection.inlineStyleObjectCount,
247
+ inlineStyleBindingCount: designEvidenceSummary.structuredInspection.inlineStyleBindingCount,
248
+ inlineStyleTokenBypassCount:
249
+ designEvidenceSummary.structuredInspection.inlineTokenBypassSignals.hardcodedColorCount
250
+ + designEvidenceSummary.structuredInspection.inlineTokenBypassSignals.rawSpacingCount
251
+ + designEvidenceSummary.structuredInspection.inlineTokenBypassSignals.rawRadiusCount
252
+ + designEvidenceSummary.structuredInspection.inlineTokenBypassSignals.rawShadowCount,
253
+ },
254
+ designEvidenceSummary,
255
+ };
256
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Static configuration and regex patterns for the lightweight static design
3
+ * evidence scan. Kept in one place so the scanner internals stay pattern-driven
4
+ * and reviewable.
5
+ */
6
+
7
+ export const FRONTEND_SCAN_DIRECTORY_NAMES = ['src', 'app', 'pages', 'components', 'styles'];
8
+ export const FRONTEND_SCAN_FILE_EXTENSIONS = new Set([
9
+ '.js', '.jsx', '.ts', '.tsx', '.vue', '.css', '.scss', '.sass',
10
+ ]);
11
+ export const FRONTEND_SCAN_IGNORE_DIRECTORY_NAMES = new Set([
12
+ '.git', 'node_modules', '.next', 'dist', 'build', 'coverage',
13
+ ]);
14
+ export const FRONTEND_FILE_SCAN_LIMIT = 200;
15
+ export const FRONTEND_FILE_SIZE_LIMIT_BYTES = 200_000;
16
+ export const DESIGN_EVIDENCE_SAMPLE_LIMIT = 12;
17
+
18
+ export const COLOR_PATTERN = /#[0-9a-fA-F]{3,8}\b|rgba?\([^)]+\)|hsla?\([^)]+\)|oklch\([^)]+\)/g;
19
+ export const PROP_DRILLING_PATTERN = /<[A-Z][A-Za-z0-9_.:-]*(?:\s+[A-Za-z0-9_:-]+=\{[^}]+\}){5,}/g;
20
+ export const MEDIA_QUERY_PATTERN = /@media\b/g;
21
+ export const TAILWIND_BREAKPOINT_PATTERN = /\b(?:sm|md|lg|xl|2xl):/g;
22
+ export const ARBITRARY_BREAKPOINT_PATTERN = /\b(?:min|max)-\[[^\]]+\]:/g;
23
+ export const CSS_VARIABLE_DEFINITION_PATTERN = /--([a-zA-Z0-9-_]+)\s*:/g;
24
+ export const CSS_VARIABLE_REFERENCE_PATTERN = /var\(--([a-zA-Z0-9-_]+)\)/g;
25
+ export const RAW_SPACING_PATTERN = /\b(?:margin|padding|gap|column-gap|row-gap|min-width|max-width|min-height|max-height|width|height|top|right|bottom|left|inset|spaceBetween|paddingInline|paddingBlock|marginInline|marginBlock|gapX|gapY|spaceX|spaceY)\b[^;\n:]*[:=]\s*['"`{(]*(-?[0-9.]+(?:px|rem|em|vh|vw))/gi;
26
+ export const RAW_RADIUS_PATTERN = /\b(?:border-radius|borderRadius)\b[^;\n:]*[:=]\s*['"`{(]*([0-9.]+(?:px|rem|em)|9999px)/gi;
27
+ export const RAW_SHADOW_PATTERN = /\b(?:box-shadow|boxShadow)\b[^;\n:]*[:=]\s*['"`{(]*([^;\n}]+)/gi;
28
+ export const FONT_FAMILY_PATTERN = /\b(?:font-family|fontFamily)\b[^;\n:]*[:=]\s*['"`{(]*([^;\n}]+)/gi;
29
+ export const FONT_SIZE_PATTERN = /\b(?:font-size|fontSize)\b[^;\n:]*[:=]\s*['"`{(]*([0-9.]+(?:px|rem|em))/gi;
30
+ export const LINE_HEIGHT_PATTERN = /\b(?:line-height|lineHeight)\b[^;\n:]*[:=]\s*['"`{(]*([0-9.]+(?:px|rem|em)?)/gi;
31
+ export const LETTER_SPACING_PATTERN = /\b(?:letter-spacing|letterSpacing)\b[^;\n:]*[:=]\s*['"`{(]*([0-9.-]+(?:px|rem|em))/gi;
32
+ export const TRANSITION_PATTERN = /\btransition(?:-[a-z]+)?\b/g;
33
+ export const ANIMATION_PATTERN = /\banimation(?:-[a-z]+)?\b/g;
34
+ export const DURATION_PATTERN = /\b\d+(?:\.\d+)?m?s\b/g;
35
+ export const MEDIA_WIDTH_PATTERN = /\((?:min|max)-width:\s*([0-9.]+(?:px|rem|em))\)/g;
36
+ export const STRING_CLASS_ATTRIBUTE_PATTERN = /\b(?:className|class)\s*=\s*(?:"([^"]*)"|'([^']*)'|\{`([^`]*)`\}|\{"([^"]*)"\}|\{'([^']*)'\})/g;
37
+ export const EXPRESSION_CLASS_ATTRIBUTE_PATTERN = /\b(?:className|class|:class)\s*=\s*\{([^}\n]+)\}/g;
38
+ export const JSX_INLINE_STYLE_PATTERN = /\bstyle\s*=\s*\{\{([\s\S]*?)\}\}/g;
39
+ export const VUE_INLINE_STYLE_PATTERN = /\b:style\s*=\s*["']\{([^"']*)\}["']/g;
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Filesystem traversal and surface-file classification for the design evidence
3
+ * scan. Bounded by FRONTEND_FILE_SCAN_LIMIT so the scan never blows up on huge
4
+ * monorepos.
5
+ */
6
+
7
+ import fs from 'node:fs/promises';
8
+ import path from 'node:path';
9
+
10
+ import {
11
+ DESIGN_EVIDENCE_SAMPLE_LIMIT,
12
+ FRONTEND_FILE_SCAN_LIMIT,
13
+ FRONTEND_SCAN_FILE_EXTENSIONS,
14
+ FRONTEND_SCAN_IGNORE_DIRECTORY_NAMES,
15
+ } from './constants.mjs';
16
+
17
+ export async function collectFrontendSourceFilePaths(directoryPath, collectedFilePaths = []) {
18
+ if (collectedFilePaths.length >= FRONTEND_FILE_SCAN_LIMIT) {
19
+ return collectedFilePaths;
20
+ }
21
+
22
+ let directoryEntries;
23
+ try {
24
+ directoryEntries = await fs.readdir(directoryPath, { withFileTypes: true });
25
+ } catch {
26
+ return collectedFilePaths;
27
+ }
28
+
29
+ for (const directoryEntry of directoryEntries) {
30
+ if (collectedFilePaths.length >= FRONTEND_FILE_SCAN_LIMIT) {
31
+ break;
32
+ }
33
+
34
+ if (directoryEntry.isDirectory()) {
35
+ if (FRONTEND_SCAN_IGNORE_DIRECTORY_NAMES.has(directoryEntry.name)) {
36
+ continue;
37
+ }
38
+
39
+ await collectFrontendSourceFilePaths(path.join(directoryPath, directoryEntry.name), collectedFilePaths);
40
+ continue;
41
+ }
42
+
43
+ const fileExtension = path.extname(directoryEntry.name).toLowerCase();
44
+ if (FRONTEND_SCAN_FILE_EXTENSIONS.has(fileExtension)) {
45
+ collectedFilePaths.push(path.join(directoryPath, directoryEntry.name));
46
+ }
47
+ }
48
+
49
+ return collectedFilePaths;
50
+ }
51
+
52
+ export function registerSurfaceFile(summary, targetDirectoryPath, scannedFilePath, seenSurfaceFiles) {
53
+ const relativeFilePath = path.relative(targetDirectoryPath, scannedFilePath).replace(/\\/g, '/');
54
+ const normalizedBaseName = path.basename(scannedFilePath, path.extname(scannedFilePath)).toLowerCase();
55
+ const looksLikeComponent = /[A-Z]/.test(path.basename(scannedFilePath, path.extname(scannedFilePath)))
56
+ || relativeFilePath.includes('/components/')
57
+ || relativeFilePath.startsWith('components/');
58
+ const looksLikePage = normalizedBaseName === 'page'
59
+ || normalizedBaseName === 'index'
60
+ || relativeFilePath.includes('/pages/')
61
+ || relativeFilePath.startsWith('pages/')
62
+ || relativeFilePath.includes('/app/');
63
+ const looksLikeLayout = normalizedBaseName === 'layout' || relativeFilePath.includes('/layouts/');
64
+
65
+ if (looksLikeComponent) {
66
+ summary.componentInventory.componentFileCount += 1;
67
+ }
68
+
69
+ if (looksLikePage) {
70
+ summary.componentInventory.pageFileCount += 1;
71
+ }
72
+
73
+ if (looksLikeLayout) {
74
+ summary.componentInventory.layoutFileCount += 1;
75
+ }
76
+
77
+ if ((looksLikeComponent || looksLikePage || looksLikeLayout) && !seenSurfaceFiles.has(relativeFilePath)) {
78
+ seenSurfaceFiles.add(relativeFilePath);
79
+ if (summary.componentInventory.surfaceFileSamples.length < DESIGN_EVIDENCE_SAMPLE_LIMIT) {
80
+ summary.componentInventory.surfaceFileSamples.push(relativeFilePath);
81
+ }
82
+ }
83
+ }