diff --git a/.gitignore b/.gitignore index e2700c0..c60b325 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ node_modules/ *.jsonl .mcp.json CLAUDE.md +/.pos-supervisor diff --git a/src/core/diagnostic-pipeline.js b/src/core/diagnostic-pipeline.js index fc4122c..f2524bf 100644 --- a/src/core/diagnostic-pipeline.js +++ b/src/core/diagnostic-pipeline.js @@ -37,6 +37,7 @@ import { existsSync, readFileSync, readdirSync } from 'node:fs'; import { join } from 'node:path'; +import yaml from 'js-yaml'; import { getKnownModulesMissingDocs } from './knowledge-loader.js'; import { buildAssetIndex, resolveAssetPath } from './asset-index.js'; import { buildTranslationIndex } from './translation-index.js'; @@ -66,80 +67,97 @@ export function runDiagnosticPipeline(result, opts) { projectDir, } = opts; + // Pipeline trace (D2 — pipeline step inspector). Each step records what changed. + const trace = []; + function traceStep(name, fn) { + const eBefore = result.errors.length; + const wBefore = result.warnings.length; + fn(); + const eRemoved = eBefore - result.errors.length; + const wRemoved = wBefore - result.warnings.length; + const eAdded = result.errors.length - (eBefore - eRemoved); + trace.push({ + step: name, + errorsRemoved: eRemoved, + warningsRemoved: wRemoved, + errorsAfter: result.errors.length, + warningsAfter: result.warnings.length, + }); + } + // Accumulate suppression summaries into one info diagnostic — the agent sees a single line. const suppressionNotes = []; + // 0. Apply user-defined suppressions from .pos-supervisor-ignore.yml (A3) + if (projectDir) { + traceStep('userSuppressions', () => applyUserSuppressions(result, filePath, projectDir)); + } + // 1. Suppress UndefinedObject for declared @param names if (docParamNames.size > 0) { - suppressDocParams(result, docParamNames); + traceStep('suppressDocParams', () => suppressDocParams(result, docParamNames)); } // 2. Suppress UnusedDocParam when param is used as named argument if (docParamNames.size > 0) { - suppressUnusedDocParams(result, docParamNames, content); + traceStep('suppressUnusedDocParams', () => suppressUnusedDocParams(result, docParamNames, content)); } // 3. Elevate Shopify contamination from warning to error - elevateShopify(result); + traceStep('elevateShopify', () => elevateShopify(result)); // 4. Deduplicate MissingRenderPartialArguments + MetadataParamsCheck - deduplicateArgChecks(result); + traceStep('deduplicateArgChecks', () => deduplicateArgChecks(result)); // 5. Suppress MetadataParamsCheck when the called target has no {% doc %} block. - // The LSP infers required params from usage patterns when no contract is declared, - // producing false positives for every optional param. Module partials (modules/*) - // are always treated as undocumented (they are excluded from lint by config AND - // overwhelmingly lack doc blocks in practice). App partials/commands/queries are - // confirmed by reading the target file from disk — if it has no {% doc %}, we - // suppress and emit an advisory info pointing at the root fix (add {% doc %}). - suppressUndocumentedTargetParams(result, content, projectDir); + traceStep('suppressUndocumentedTargetParams', () => suppressUndocumentedTargetParams(result, content, projectDir)); // 6. Suppress required-param diagnostics whose target partial defaults the param. - // The target's {% doc %} declared it required, but its body does `| default:`, - // so callers that omit the param still receive a valid value. This covers the - // common pattern where authors forgot to bracket the @param name. - suppressRequiredParamsWithDefault(result, content, projectDir); + traceStep('suppressRequiredParamsWithDefault', () => suppressRequiredParamsWithDefault(result, content, projectDir)); // 7. Suppress DeprecatedTag for module helper includes - suppressModuleHelpers(result, content); + traceStep('suppressModuleHelpers', () => suppressModuleHelpers(result, content)); - // 8. Suppress OrphanedPartial for commands/queries and for partials in - // multi-file creation plans (callers may be pending and not on disk yet). - suppressOrphanedPartial(result, filePath, pendingFiles, pendingPages); + // 8. Suppress OrphanedPartial for commands/queries and pending plans + traceStep('suppressOrphanedPartial', () => suppressOrphanedPartial(result, filePath, pendingFiles, pendingPages)); - // 8. Suppress MissingPartial for pending files + // 9. Suppress MissingPartial for pending files if (pendingFiles.length > 0) { - const n = suppressByPending(result, { - check: 'MissingPartial', - pendingSet: buildPendingPartialNames(pendingFiles), - extractKey: (d) => d.message?.match(/['"]([^'"]+)['"]/)?.[1] ?? null, + traceStep('suppressPendingPartials', () => { + const n = suppressByPending(result, { + check: 'MissingPartial', + pendingSet: buildPendingPartialNames(pendingFiles), + extractKey: (d) => d.message?.match(/['"]([^'"]+)['"]/)?.[1] ?? null, + }); + if (n > 0) suppressionNotes.push(`${n} MissingPartial(s) for pending files`); }); - if (n > 0) suppressionNotes.push(`${n} MissingPartial(s) for pending files`); } - // 9. Suppress MissingPage for pending pages + // 10. Suppress MissingPage for pending pages if (pendingPages.length > 0) { - const n = suppressByPending(result, { - check: 'MissingPage', - pendingSet: buildPendingPageKeys(pendingPages), - extractKey: (d) => { - // MissingPage messages look like: Page 'blog_posts/show' not found - // or: Missing page at slug 'blog_posts' - const m = d.message?.match(/['"]([^'"]+)['"]/); - return m ? m[1] : null; - }, + traceStep('suppressPendingPages', () => { + const n = suppressByPending(result, { + check: 'MissingPage', + pendingSet: buildPendingPageKeys(pendingPages), + extractKey: (d) => { + const m = d.message?.match(/['"]([^'"]+)['"]/); + return m ? m[1] : null; + }, + }); + if (n > 0) suppressionNotes.push(`${n} MissingPage(s) for pending pages`); }); - if (n > 0) suppressionNotes.push(`${n} MissingPage(s) for pending pages`); } - // 10. Suppress TranslationKeyExists for pending translations + // 11. Suppress TranslationKeyExists for pending translations if (pendingTranslations.length > 0) { - const n = suppressByPending(result, { - check: 'TranslationKeyExists', - pendingSet: new Set(pendingTranslations), - extractKey: (d) => d.message?.match(/['"]([^'"]+)['"]/)?.[1] ?? null, + traceStep('suppressPendingTranslations', () => { + const n = suppressByPending(result, { + check: 'TranslationKeyExists', + pendingSet: new Set(pendingTranslations), + extractKey: (d) => d.message?.match(/['"]([^'"]+)['"]/)?.[1] ?? null, + }); + if (n > 0) suppressionNotes.push(`${n} TranslationKeyExists for pending translations`); }); - if (n > 0) suppressionNotes.push(`${n} TranslationKeyExists for pending translations`); } if (suppressionNotes.length > 0) { @@ -150,44 +168,69 @@ export function runDiagnosticPipeline(result, opts) { }); } - // 11. Verify MissingAsset against filesystem + // 12. Verify MissingAsset against filesystem if (projectDir) { - verifyMissingAssets(result, projectDir); + traceStep('verifyMissingAssets', () => verifyMissingAssets(result, projectDir)); } - // 12. Verify TranslationKeyExists against filesystem. The LSP's translation - // cache lags behind disk just like its asset cache — after the agent - // writes a key to app/translations/.yml the LSP keeps reporting - // "key not found" until it re-indexes. Cross-check against the real - // YAML files so the agent does not need to pass `pending_translations` - // for keys that already exist on disk. + // 13. Verify TranslationKeyExists against filesystem if (projectDir) { - verifyTranslationKeysOnDisk(result, projectDir); + traceStep('verifyTranslationKeysOnDisk', () => verifyTranslationKeysOnDisk(result, projectDir)); } - // 13. Verify MissingPage against filesystem. validate_code analyses one - // file at a time, so any link in a partial pointing to a route defined - // in OTHER pages fires MissingPage. Cross-check against the real page - // files (slug from frontmatter or path-derived) so a header partial - // linking to /notes does not flag the route as missing when - // app/views/pages/notes/index.html.liquid clearly exists. + // 14. Verify MissingPage against filesystem if (projectDir) { - verifyPageRoutesOnDisk(result, projectDir); + traceStep('verifyPageRoutesOnDisk', () => verifyPageRoutesOnDisk(result, projectDir)); } - // 14. Verify OrphanedPartial against filesystem. validate_code analyses - // one file at a time, so the checker has no cross-file render graph. - // After scaffold(write:true) writes all files and clears pending state, - // the checker still reports OrphanedPartial because its index hasn't - // re-indexed the new pages yet. Cross-check by scanning all .liquid - // files on disk for a render/function reference to this partial. + // 15. Verify OrphanedPartial against filesystem if (projectDir) { - verifyOrphanedPartialOnDisk(result, filePath, projectDir); + traceStep('verifyOrphanedPartialOnDisk', () => verifyOrphanedPartialOnDisk(result, filePath, projectDir)); } + + // Attach pipeline trace for dashboard inspector (D2) + result._pipelineTrace = trace; } // ── Individual filters ────────────────────────────────────────────────────── +function applyUserSuppressions(result, filePath, projectDir) { + const suppressFile = join(projectDir, '.pos-supervisor-ignore.yml'); + if (!existsSync(suppressFile)) return; + let rules; + try { + const parsed = yaml.load(readFileSync(suppressFile, 'utf-8')); + rules = parsed?.suppressions; + } catch { return; } + if (!Array.isArray(rules) || rules.length === 0) return; + + const matchRule = (d) => rules.some(r => { + if (r.check !== d.check) return false; + if (r.file_pattern) { + if (r.file_pattern.includes('*')) { + const re = new RegExp('^' + r.file_pattern.replace(/\*/g, '.*') + '$'); + if (!re.test(filePath)) return false; + } else if (!filePath.includes(r.file_pattern)) { + return false; + } + } + return true; + }); + + const errBefore = result.errors.length; + const warnBefore = result.warnings.length; + result.errors = result.errors.filter(d => !matchRule(d)); + result.warnings = result.warnings.filter(d => !matchRule(d)); + const suppressed = (errBefore - result.errors.length) + (warnBefore - result.warnings.length); + if (suppressed > 0) { + result.infos.push({ + check: 'pos-supervisor:UserSuppressed', + severity: 'info', + message: `Suppressed ${suppressed} diagnostic(s) via .pos-supervisor-ignore.yml`, + }); + } +} + function suppressDocParams(result, docParamNames) { const match = (diag) => { if (diag.check !== 'UndefinedObject') return false; diff --git a/src/core/project-scanner.js b/src/core/project-scanner.js index 0aa310a..6ceb6f9 100644 --- a/src/core/project-scanner.js +++ b/src/core/project-scanner.js @@ -111,6 +111,7 @@ export async function scanProject(projectDir) { queries, pages, partials, + layouts, translations, assets, summary: { diff --git a/src/dashboard.js b/src/dashboard.js index 9c88ea9..fbbf6f0 100644 --- a/src/dashboard.js +++ b/src/dashboard.js @@ -3,12 +3,12 @@ * Served at GET /dashboard. * * Features: - * - Real-time via SSE (no polling for activity) - * - Timeline strip: visual tool call sequence with duration as width - * - File validation map: per-file error state grid - * - Compliance checklist: workflow health at a glance - * - Activity table: file_path + error/warning counts in detail column - * - Stats, Playground, Knowledge browser, LSP controls + * - Real-time via SSE (no polling for activity) + * - Timeline strip: visual tool call sequence with duration as width + * - File validation map: per-file error state grid + * - Compliance checklist: workflow health at a glance + * - Activity table: file_path + error/warning counts in detail column + * - Stats, Playground, Knowledge browser, LSP controls */ export function buildDashboardHtml() { @@ -20,308 +20,626 @@ export function buildDashboardHtml() { pos-supervisor
+

pos-supervisor

@@ -329,26 +647,36 @@ export function buildDashboardHtml() {
-
LSPwarming up
-
pos-clichecking
-
tools
-
version
-
calls0
-
errors0
-
connecting
+
+ HEALTH : +
+ + + + + +
+
+
LSP : WAIT
+
POS-CLI : WAIT
+
TOOLS :
+
VER :
+
CALLS : 0
+
ERR : 0
+
+
CONNECTING
Overview
+
Activity
Explorer
-
Routes
Health
-
Activity
-
Stats
-
Playground
-
Knowledge
-
POS-CLI
+
Tool Insights
+
Tool Lab
LSP
+
POS-CLI
+
@@ -372,7 +700,7 @@ export function buildDashboardHtml() {
-

Error Patterns (this session)

+

Error Patterns (SESSION)

no validate_code calls yet
@@ -383,38 +711,101 @@ export function buildDashboardHtml() { - + +
-
- - -
-
-
Click Refresh or switch to this tab to load project data.
-
- -
-
- -
-
Click Refresh to load route data.
+
+

Project Map

+
Runs project_map — vertical slices (schema → GraphQL → business logic → pages).
+
+ + +
+
+
Execute FETCH PROJECT MAP to load resources.
+
+ + + + + +
+

Routes & Lifecycle

+
Execute FETCH PROJECT MAP above to load route tables.
+
+ +
+

Dependency Impact Tree

+
Click a file to see what it depends on and what depends on it. Colored by validation state — fixing red files unblocks everything that references them.
+
+ + +
+ +
- +
-
- - -
-
Click Refresh to load project health data.
+ +
+

Project Analysis

+
Runs analyze_project for stuck files, dead code, integrity, orphans, cycles. Pair with the Project Map in the Explorer tab to interpret findings in context.
+
+ + +
+
Execute RUN ANALYSIS to load project health data.
+
+ +
+

Suppressions

+
Rules written to .pos-supervisor-ignore.yml. The diagnostic pipeline drops matching checks before enrichment.
+
LOADING SUPPRESSIONS...
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
- +
-

Timeline

+

Timeline

@@ -428,29 +819,29 @@ export function buildDashboardHtml() {
-
- - - - -
- - - - - - - - - - - - -
TimeToolFile / DetailIssuesDurationStatus
loading…
-
+
+

Call Log

+
+ + + + +
+ + + + + + + + + + + + +
TimeToolFile / DetailIssuesDurationStatus
awaiting entries…
+
- -

Tool Usage

@@ -458,51 +849,27 @@ export function buildDashboardHtml() {
no calls yet
+

Check Frequency

no validate_code calls yet
-
- -
-
-
-

Select Tool

-
loading…
-
-
-
- - - -
-
- - +
+

Session History

+
Previous sessions are persisted to .pos-supervisor/sessions/. Click SAVE CURRENT to snapshot now, or LOAD SESSIONS to compare past runs side-by-side.
+
+ + +
-
-
+
CLICK LOAD SESSIONS TO FETCH SESSION HISTORY.
+
+ - -
-
-
-

Hints

-
loading…
-
-
-
Select a hint to view its content.
-
-
+
@@ -518,7 +885,7 @@ export function buildDashboardHtml() {
- +
@@ -540,7 +907,7 @@ export function buildDashboardHtml() {
- +
@@ -552,6 +919,120 @@ export function buildDashboardHtml() {
+ +
+
+ + +
+
+
+
Diagnostic Effectiveness
+
For every check the LSP fires, this shows how often a later validation makes it go away. LOW FIX RATE = the agent sees the error but doesn't fix it — the hint/fix text is probably wrong or missing. Action: improve the hint in src/data/hints/<check>.md.
+
No validate_code calls yet — effectiveness data appears after files are validated multiple times.
+
+
+
Hint Effectiveness
+
Counts how many times enrich_error was called for a check, and how many of those calls led to the error being fixed on the next validation. LOW CONVERSION = agent read the hint but didn't act on it. Action: rewrite the hint to be more directive.
+
No hint data yet — call enrich_error on a few errors first.
+
+
+
Per-File Diagnostic Diff
+
For each file validated more than once, shows what changed between runs: RESOLVED checks, NEW checks, STILL PRESENT. Use it to see if the last edit helped or made things worse.
+
No files with multiple validations yet.
+
+
+
Knowledge Gaps
+
Lists LSP checks that fired in this session but have no hint file in src/data/hints/. Agents got an error message with no guidance on how to fix it. Action: add a hint file for each listed check.
+
Loading…
+
+
+
Workflow Patterns
+
Common tool-call sequences observed in this session (e.g. project_map → scaffold → validate_intent → validate_code). Confirms whether agents are following the intended workflow or taking shortcuts.
+
No tool calls yet.
+
+
+
Scaffold Quality
+
Every scaffold call is scored on: files generated, conflicts, and whether the follow-up validate_intent passed. LOW SCORE = scaffold produced output that didn't match intent. Action: improve the scaffold template or the validator's ontology.
+
No scaffold calls in this session.
+
+
+
Pipeline Inspector
+
Per-file trace through the diagnostic-pipeline — shows how many errors/warnings each step removed. Use it to find inactive steps (never triggered → candidates for removal) and over-eager steps (suppressing too much). Click a file header to expand the trace.
+
No pipeline trace data yet.
+
+
+
Knowledge Library
+
The hint files in src/data/hints/ rendered into the hint field of every enriched diagnostic. Use this to review what agents will actually see when a check fires.
+
+
+

Hints

+
loading…
+
+
+
Select a hint to view its content.
+
+
+
+
+ + +
+
+ Every tool exposed by this server — description, input schema, live usage stats, and an executor. Select a tool to see its docs and run it; toggle Live Diagnostic Console to validate arbitrary or project content without wiring params. +
+ +
+ +
+ + +
+
+

Tools

+
LOADING...
+
+
+
+
SELECT A TOOL ON THE LEFT TO VIEW DOCS, SESSION STATS, AND RUN IT.
+
+ +
+
+
+
@@ -560,25 +1041,29 @@ export function buildDashboardHtml() {
Status
-
initialising
+
WAIT
pos-cli
-
checking
+
WAIT
- +
-

LSP Events (this session)

+

Daemon Log (SESSION)

no events yet
+ +
+ +