@codeyam/codeyam-cli 0.1.20 → 0.1.22

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 (130) hide show
  1. package/analyzer-template/.build-info.json +7 -7
  2. package/analyzer-template/log.txt +3 -3
  3. package/analyzer-template/package.json +1 -1
  4. package/analyzer-template/packages/ai/src/lib/dataStructure/ScopeDataStructure.ts +36 -9
  5. package/analyzer-template/packages/ai/src/lib/dataStructure/equivalencyManagers/ParentScopeManager.ts +10 -3
  6. package/analyzer-template/packages/ai/src/lib/dataStructure/helpers/cleanKnownObjectFunctions.ts +16 -6
  7. package/analyzer-template/packages/analyze/index.ts +4 -1
  8. package/analyzer-template/packages/analyze/src/lib/files/analyze/analyzeEntities/prepareDataStructures.ts +28 -2
  9. package/analyzer-template/packages/analyze/src/lib/files/analyze/analyzeEntities.ts +5 -36
  10. package/analyzer-template/packages/analyze/src/lib/files/analyze/findOrCreateEntity.ts +1 -0
  11. package/analyzer-template/packages/analyze/src/lib/files/analyze/trackEntityCircularDependencies.ts +21 -0
  12. package/analyzer-template/packages/analyze/src/lib/files/analyze/validateDependencyAnalyses.ts +82 -10
  13. package/analyzer-template/packages/analyze/src/lib/files/analyzeChange.ts +4 -0
  14. package/analyzer-template/packages/analyze/src/lib/files/analyzeInitial.ts +4 -0
  15. package/analyzer-template/packages/analyze/src/lib/files/analyzeNextRoute.ts +8 -3
  16. package/analyzer-template/packages/analyze/src/lib/files/scenarios/generateDataStructure.ts +235 -58
  17. package/analyzer-template/packages/analyze/src/lib/files/scenarios/mergeInDependentDataStructure.ts +170 -26
  18. package/analyzer-template/packages/aws/package.json +1 -1
  19. package/analyzer-template/packages/database/src/lib/loadEntity.ts +11 -4
  20. package/analyzer-template/packages/github/dist/database/src/lib/loadEntity.d.ts +4 -1
  21. package/analyzer-template/packages/github/dist/database/src/lib/loadEntity.d.ts.map +1 -1
  22. package/analyzer-template/packages/github/dist/database/src/lib/loadEntity.js +4 -4
  23. package/analyzer-template/packages/github/dist/database/src/lib/loadEntity.js.map +1 -1
  24. package/analyzer-template/packages/utils/dist/utils/src/lib/fs/rsyncCopy.d.ts +3 -1
  25. package/analyzer-template/packages/utils/dist/utils/src/lib/fs/rsyncCopy.d.ts.map +1 -1
  26. package/analyzer-template/packages/utils/dist/utils/src/lib/fs/rsyncCopy.js +22 -1
  27. package/analyzer-template/packages/utils/dist/utils/src/lib/fs/rsyncCopy.js.map +1 -1
  28. package/analyzer-template/packages/utils/src/lib/fs/rsyncCopy.ts +27 -0
  29. package/codeyam-cli/src/commands/__tests__/editor.auditNoAutoAnalysis.test.js +63 -0
  30. package/codeyam-cli/src/commands/__tests__/editor.auditNoAutoAnalysis.test.js.map +1 -0
  31. package/codeyam-cli/src/commands/editor.js +553 -93
  32. package/codeyam-cli/src/commands/editor.js.map +1 -1
  33. package/codeyam-cli/src/utils/__tests__/editorAudit.test.js +991 -31
  34. package/codeyam-cli/src/utils/__tests__/editorAudit.test.js.map +1 -1
  35. package/codeyam-cli/src/utils/__tests__/editorPreview.test.js +11 -3
  36. package/codeyam-cli/src/utils/__tests__/editorPreview.test.js.map +1 -1
  37. package/codeyam-cli/src/utils/__tests__/entityChangeStatus.test.js +33 -1
  38. package/codeyam-cli/src/utils/__tests__/entityChangeStatus.test.js.map +1 -1
  39. package/codeyam-cli/src/utils/__tests__/manualEntityAnalysis.test.js +302 -0
  40. package/codeyam-cli/src/utils/__tests__/manualEntityAnalysis.test.js.map +1 -0
  41. package/codeyam-cli/src/utils/__tests__/registerScenarioResult.test.js +127 -0
  42. package/codeyam-cli/src/utils/__tests__/registerScenarioResult.test.js.map +1 -0
  43. package/codeyam-cli/src/utils/__tests__/testRunner.test.js +217 -0
  44. package/codeyam-cli/src/utils/__tests__/testRunner.test.js.map +1 -0
  45. package/codeyam-cli/src/utils/analysisRunner.js +28 -1
  46. package/codeyam-cli/src/utils/analysisRunner.js.map +1 -1
  47. package/codeyam-cli/src/utils/analyzer.js +11 -1
  48. package/codeyam-cli/src/utils/analyzer.js.map +1 -1
  49. package/codeyam-cli/src/utils/editorAudit.js +210 -14
  50. package/codeyam-cli/src/utils/editorAudit.js.map +1 -1
  51. package/codeyam-cli/src/utils/editorPreview.js +5 -3
  52. package/codeyam-cli/src/utils/editorPreview.js.map +1 -1
  53. package/codeyam-cli/src/utils/entityChangeStatus.server.js +16 -0
  54. package/codeyam-cli/src/utils/entityChangeStatus.server.js.map +1 -1
  55. package/codeyam-cli/src/utils/manualEntityAnalysis.js +196 -0
  56. package/codeyam-cli/src/utils/manualEntityAnalysis.js.map +1 -0
  57. package/codeyam-cli/src/utils/queue/job.js +20 -2
  58. package/codeyam-cli/src/utils/queue/job.js.map +1 -1
  59. package/codeyam-cli/src/utils/registerScenarioResult.js +52 -0
  60. package/codeyam-cli/src/utils/registerScenarioResult.js.map +1 -0
  61. package/codeyam-cli/src/utils/testRunner.js +199 -1
  62. package/codeyam-cli/src/utils/testRunner.js.map +1 -1
  63. package/codeyam-cli/src/webserver/__tests__/idleDetector.test.js +35 -0
  64. package/codeyam-cli/src/webserver/__tests__/idleDetector.test.js.map +1 -1
  65. package/codeyam-cli/src/webserver/app/lib/clientErrors.js +3 -0
  66. package/codeyam-cli/src/webserver/app/lib/clientErrors.js.map +1 -1
  67. package/codeyam-cli/src/webserver/build/client/assets/MiniClaudeChat-CQENLSrF.js +36 -0
  68. package/codeyam-cli/src/webserver/build/client/assets/api.editor-save-scenario-data-l0sNRNKZ.js +1 -0
  69. package/codeyam-cli/src/webserver/build/client/assets/api.editor-schema-l0sNRNKZ.js +1 -0
  70. package/codeyam-cli/src/webserver/build/client/assets/cy-logo-cli-Coe5NhbS.js +1 -0
  71. package/codeyam-cli/src/webserver/build/client/assets/{cy-logo-cli-CCKUIm0S.svg → cy-logo-cli-DoA97ML3.svg} +2 -2
  72. package/codeyam-cli/src/webserver/build/client/assets/editor.entity.(_sha)-aIHKLB-m.js +96 -0
  73. package/codeyam-cli/src/webserver/build/client/assets/editorPreview-CluPkvXJ.js +41 -0
  74. package/codeyam-cli/src/webserver/build/client/assets/{entity._sha._-Blfy9UlN.js → entity._sha._-ByHz6rAQ.js} +13 -12
  75. package/codeyam-cli/src/webserver/build/client/assets/{entity._sha.scenarios._scenarioId.dev-KTQuL0aj.js → entity._sha.scenarios._scenarioId.dev-CmLO432x.js} +1 -1
  76. package/codeyam-cli/src/webserver/build/client/assets/{entity._sha.scenarios._scenarioId.fullscreen-C6eeL24i.js → entity._sha.scenarios._scenarioId.fullscreen-Bz9sCUF_.js} +1 -1
  77. package/codeyam-cli/src/webserver/build/client/assets/globals-oyPmV37k.css +1 -0
  78. package/codeyam-cli/src/webserver/build/client/assets/manifest-bcbb3d49.js +1 -0
  79. package/codeyam-cli/src/webserver/build/client/assets/{root-BxUQigda.js → root-D2_tktnk.js} +26 -13
  80. package/codeyam-cli/src/webserver/build/server/assets/analysisRunner-DjF-soOH.js +16 -0
  81. package/codeyam-cli/src/webserver/build/server/assets/{index-CjLhfz6Z.js → index-nAvHGWbz.js} +1 -1
  82. package/codeyam-cli/src/webserver/build/server/assets/{init-BEqlbI84.js → init-XhpIt-OT.js} +1 -1
  83. package/codeyam-cli/src/webserver/build/server/assets/server-build-DVwiibFu.js +644 -0
  84. package/codeyam-cli/src/webserver/build/server/index.js +1 -1
  85. package/codeyam-cli/src/webserver/build-info.json +5 -5
  86. package/codeyam-cli/src/webserver/idleDetector.js +15 -0
  87. package/codeyam-cli/src/webserver/idleDetector.js.map +1 -1
  88. package/codeyam-cli/src/webserver/terminalServer.js +18 -5
  89. package/codeyam-cli/src/webserver/terminalServer.js.map +1 -1
  90. package/codeyam-cli/templates/skills/codeyam-editor/SKILL.md +2 -2
  91. package/package.json +1 -1
  92. package/packages/ai/src/lib/dataStructure/ScopeDataStructure.js +27 -10
  93. package/packages/ai/src/lib/dataStructure/ScopeDataStructure.js.map +1 -1
  94. package/packages/ai/src/lib/dataStructure/equivalencyManagers/ParentScopeManager.js +9 -2
  95. package/packages/ai/src/lib/dataStructure/equivalencyManagers/ParentScopeManager.js.map +1 -1
  96. package/packages/ai/src/lib/dataStructure/helpers/cleanKnownObjectFunctions.js +14 -4
  97. package/packages/ai/src/lib/dataStructure/helpers/cleanKnownObjectFunctions.js.map +1 -1
  98. package/packages/analyze/index.js +1 -1
  99. package/packages/analyze/index.js.map +1 -1
  100. package/packages/analyze/src/lib/files/analyze/analyzeEntities/prepareDataStructures.js +16 -2
  101. package/packages/analyze/src/lib/files/analyze/analyzeEntities/prepareDataStructures.js.map +1 -1
  102. package/packages/analyze/src/lib/files/analyze/analyzeEntities.js +6 -26
  103. package/packages/analyze/src/lib/files/analyze/analyzeEntities.js.map +1 -1
  104. package/packages/analyze/src/lib/files/analyze/findOrCreateEntity.js +1 -0
  105. package/packages/analyze/src/lib/files/analyze/findOrCreateEntity.js.map +1 -1
  106. package/packages/analyze/src/lib/files/analyze/trackEntityCircularDependencies.js +14 -0
  107. package/packages/analyze/src/lib/files/analyze/trackEntityCircularDependencies.js.map +1 -1
  108. package/packages/analyze/src/lib/files/analyze/validateDependencyAnalyses.js +44 -11
  109. package/packages/analyze/src/lib/files/analyze/validateDependencyAnalyses.js.map +1 -1
  110. package/packages/analyze/src/lib/files/analyzeChange.js +1 -0
  111. package/packages/analyze/src/lib/files/analyzeChange.js.map +1 -1
  112. package/packages/analyze/src/lib/files/analyzeInitial.js +1 -0
  113. package/packages/analyze/src/lib/files/analyzeInitial.js.map +1 -1
  114. package/packages/analyze/src/lib/files/analyzeNextRoute.js +5 -1
  115. package/packages/analyze/src/lib/files/analyzeNextRoute.js.map +1 -1
  116. package/packages/analyze/src/lib/files/scenarios/generateDataStructure.js +116 -28
  117. package/packages/analyze/src/lib/files/scenarios/generateDataStructure.js.map +1 -1
  118. package/packages/analyze/src/lib/files/scenarios/mergeInDependentDataStructure.js +139 -24
  119. package/packages/analyze/src/lib/files/scenarios/mergeInDependentDataStructure.js.map +1 -1
  120. package/packages/database/src/lib/loadEntity.js +4 -4
  121. package/packages/database/src/lib/loadEntity.js.map +1 -1
  122. package/packages/utils/src/lib/fs/rsyncCopy.js +22 -1
  123. package/packages/utils/src/lib/fs/rsyncCopy.js.map +1 -1
  124. package/codeyam-cli/src/webserver/build/client/assets/cy-logo-cli-DcX-ZS3p.js +0 -1
  125. package/codeyam-cli/src/webserver/build/client/assets/editor.entity.(_sha)-DII1pg_z.js +0 -58
  126. package/codeyam-cli/src/webserver/build/client/assets/editorPreview-oepecPae.js +0 -41
  127. package/codeyam-cli/src/webserver/build/client/assets/globals-Yn9W3zp3.css +0 -1
  128. package/codeyam-cli/src/webserver/build/client/assets/manifest-cdf2c0a7.js +0 -1
  129. package/codeyam-cli/src/webserver/build/server/assets/analysisRunner-B_PsTAb1.js +0 -13
  130. package/codeyam-cli/src/webserver/build/server/assets/server-build-YI63xTu4.js +0 -553
@@ -1,6 +1,6 @@
1
1
  import Database from 'better-sqlite3';
2
2
  import { Kysely, SqliteDialect } from 'kysely';
3
- import { isComponent, classifyGlossaryEntries, computeAudit, filterGlossaryByChangeStatus, resolveAuditSessionScope, queryScenarioCounts, queryPageScenarioCounts, queryIncompleteEntities, queryMiscategorizedScenarios, queryUnassociatedScenarios, isOnlyIncompleteEntities, isAutoRemediable, identifyScenariosNeedingRecapture, detectDuplicateNames, } from "../editorAudit.js";
3
+ import { isComponent, classifyGlossaryEntries, computeAudit, filterGlossaryByChangeStatus, resolveAuditSessionScope, queryScenarioCounts, queryPageScenarioCounts, queryIncompleteEntities, queryMiscategorizedScenarios, queryUnassociatedScenarios, isOnlyIncompleteEntities, isOnlyPreExistingIncomplete, isAutoRemediable, identifyScenariosNeedingRecapture, detectDuplicateNames, aggregateClientErrorsByComponent, } from "../editorAudit.js";
4
4
  describe('editorAudit', () => {
5
5
  describe('isComponent', () => {
6
6
  it('should return true for JSX.Element return type', () => {
@@ -1466,6 +1466,279 @@ describe('editorAudit', () => {
1466
1466
  expect(auditResult.summary.incompleteEntities).toBeUndefined();
1467
1467
  });
1468
1468
  });
1469
+ // ── filterToIncompleteFilePaths ──────────────────────────────────────
1470
+ describe('filterToIncompleteFilePaths', () => {
1471
+ // analyze-imports processes ALL file paths (~120 files) even when only
1472
+ // a few need analysis. This function filters to files that have no
1473
+ // entity with an analysis record.
1474
+ let db;
1475
+ let rawDb;
1476
+ const projectId = 'test-project-id';
1477
+ beforeEach(async () => {
1478
+ rawDb = new Database(':memory:');
1479
+ db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
1480
+ await db.schema
1481
+ .createTable('analyses')
1482
+ .addColumn('id', 'varchar', (col) => col.primaryKey())
1483
+ .addColumn('entity_sha', 'varchar')
1484
+ .addColumn('entity_name', 'varchar')
1485
+ .addColumn('project_id', 'varchar')
1486
+ .execute();
1487
+ await db.schema
1488
+ .createTable('entities')
1489
+ .addColumn('sha', 'varchar', (col) => col.primaryKey())
1490
+ .addColumn('name', 'varchar')
1491
+ .addColumn('entity_type', 'varchar')
1492
+ .addColumn('file_path', 'varchar')
1493
+ .execute();
1494
+ });
1495
+ afterEach(async () => {
1496
+ await db.destroy();
1497
+ });
1498
+ it('should exclude files that have an entity with an analysis', async () => {
1499
+ const { filterToIncompleteFilePaths } = require('../editorAudit');
1500
+ // Entity with analysis — skip
1501
+ await db
1502
+ .insertInto('entities')
1503
+ .values({
1504
+ sha: 'sha-header',
1505
+ name: 'Header',
1506
+ entity_type: 'visual',
1507
+ file_path: 'app/components/Header.tsx',
1508
+ })
1509
+ .execute();
1510
+ await db
1511
+ .insertInto('analyses')
1512
+ .values({
1513
+ id: 'a-1',
1514
+ entity_sha: 'sha-header',
1515
+ entity_name: 'Header',
1516
+ project_id: projectId,
1517
+ })
1518
+ .execute();
1519
+ // Entity without analysis — needs analysis
1520
+ await db
1521
+ .insertInto('entities')
1522
+ .values({
1523
+ sha: 'sha-rule',
1524
+ name: 'RuleBuilder',
1525
+ entity_type: 'visual',
1526
+ file_path: 'app/components/RuleBuilder.tsx',
1527
+ })
1528
+ .execute();
1529
+ const allFilePaths = [
1530
+ 'app/components/Header.tsx',
1531
+ 'app/components/RuleBuilder.tsx',
1532
+ 'app/components/Footer.tsx', // no entity — needs analysis
1533
+ ];
1534
+ const result = await filterToIncompleteFilePaths(db, projectId, allFilePaths);
1535
+ expect(result).toContain('app/components/RuleBuilder.tsx');
1536
+ expect(result).toContain('app/components/Footer.tsx');
1537
+ expect(result).not.toContain('app/components/Header.tsx');
1538
+ });
1539
+ it('should return all file paths when no entities exist yet', async () => {
1540
+ const { filterToIncompleteFilePaths } = require('../editorAudit');
1541
+ const filePaths = ['app/Foo.tsx', 'app/Bar.tsx'];
1542
+ const result = await filterToIncompleteFilePaths(db, projectId, filePaths);
1543
+ expect(result).toEqual(filePaths);
1544
+ });
1545
+ it('should return empty when all files have analyzed entities', async () => {
1546
+ const { filterToIncompleteFilePaths } = require('../editorAudit');
1547
+ await db
1548
+ .insertInto('entities')
1549
+ .values({
1550
+ sha: 'sha-a',
1551
+ name: 'CompA',
1552
+ entity_type: 'visual',
1553
+ file_path: 'app/CompA.tsx',
1554
+ })
1555
+ .execute();
1556
+ await db
1557
+ .insertInto('analyses')
1558
+ .values({
1559
+ id: 'a-1',
1560
+ entity_sha: 'sha-a',
1561
+ entity_name: 'CompA',
1562
+ project_id: projectId,
1563
+ })
1564
+ .execute();
1565
+ const result = await filterToIncompleteFilePaths(db, projectId, [
1566
+ 'app/CompA.tsx',
1567
+ ]);
1568
+ expect(result).toEqual([]);
1569
+ });
1570
+ it('should skip files with analysis even without scenarios', async () => {
1571
+ const { filterToIncompleteFilePaths } = require('../editorAudit');
1572
+ // Entity with analysis but NO scenarios — still complete
1573
+ await db
1574
+ .insertInto('entities')
1575
+ .values({
1576
+ sha: 'sha-util',
1577
+ name: 'utils',
1578
+ entity_type: 'library',
1579
+ file_path: 'app/lib/utils.ts',
1580
+ })
1581
+ .execute();
1582
+ await db
1583
+ .insertInto('analyses')
1584
+ .values({
1585
+ id: 'a-1',
1586
+ entity_sha: 'sha-util',
1587
+ entity_name: 'utils',
1588
+ project_id: projectId,
1589
+ })
1590
+ .execute();
1591
+ const result = await filterToIncompleteFilePaths(db, projectId, [
1592
+ 'app/lib/utils.ts',
1593
+ ]);
1594
+ expect(result).toEqual([]);
1595
+ });
1596
+ });
1597
+ // ── phantom entity SHAs ─────────────────────────────────────────────
1598
+ describe('queryIncompleteEntities with phantom entity SHAs', () => {
1599
+ // Root cause of the Margo/reader step-blocking bug:
1600
+ // Scenarios registered without component_path got a phantom entity_sha
1601
+ // computed from component_name alone. These SHAs have NO entity record
1602
+ // in the entities table and NO analyses. syncScenarioEntityShas can't
1603
+ // fix them (it skips scenarios without component_path). So they remain
1604
+ // "incomplete" forever, blocking step progression.
1605
+ //
1606
+ // Fix: queryIncompleteEntities should not report scenarios whose
1607
+ // entity_sha has no entity record — these are orphaned data, not
1608
+ // fixable by running analyze-imports.
1609
+ let db;
1610
+ let rawDb;
1611
+ const projectId = 'test-project-id';
1612
+ beforeEach(async () => {
1613
+ rawDb = new Database(':memory:');
1614
+ db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
1615
+ await db.schema
1616
+ .createTable('editor_scenarios')
1617
+ .addColumn('id', 'varchar', (col) => col.primaryKey())
1618
+ .addColumn('project_id', 'varchar', (col) => col.notNull())
1619
+ .addColumn('name', 'varchar', (col) => col.notNull())
1620
+ .addColumn('component_name', 'varchar')
1621
+ .addColumn('component_path', 'varchar')
1622
+ .addColumn('entity_sha', 'varchar')
1623
+ .addColumn('display_name', 'varchar')
1624
+ .addColumn('page_file_path', 'varchar')
1625
+ .addColumn('url', 'varchar')
1626
+ .addColumn('created_at', 'datetime')
1627
+ .addColumn('updated_at', 'datetime')
1628
+ .execute();
1629
+ await db.schema
1630
+ .createTable('analyses')
1631
+ .addColumn('id', 'varchar', (col) => col.primaryKey())
1632
+ .addColumn('entity_sha', 'varchar')
1633
+ .addColumn('entity_name', 'varchar')
1634
+ .addColumn('project_id', 'varchar')
1635
+ .execute();
1636
+ await db.schema
1637
+ .createTable('entities')
1638
+ .addColumn('sha', 'varchar', (col) => col.primaryKey())
1639
+ .addColumn('name', 'varchar')
1640
+ .addColumn('entity_type', 'varchar')
1641
+ .addColumn('file_path', 'varchar')
1642
+ .execute();
1643
+ });
1644
+ afterEach(async () => {
1645
+ await db.destroy();
1646
+ });
1647
+ it('should not report scenarios with phantom entity SHAs (no entity record exists)', async () => {
1648
+ // Real entity — has an entity record, analyses, and scenarios. Complete.
1649
+ await db
1650
+ .insertInto('entities')
1651
+ .values({
1652
+ sha: 'sha-real',
1653
+ name: 'RuleBuilder',
1654
+ entity_type: 'visual',
1655
+ file_path: 'app/components/RuleBuilder.tsx',
1656
+ })
1657
+ .execute();
1658
+ await db
1659
+ .insertInto('analyses')
1660
+ .values({
1661
+ id: 'a-1',
1662
+ entity_sha: 'sha-real',
1663
+ entity_name: 'RuleBuilder',
1664
+ project_id: projectId,
1665
+ })
1666
+ .execute();
1667
+ await db
1668
+ .insertInto('editor_scenarios')
1669
+ .values({
1670
+ id: 'sc-good',
1671
+ project_id: projectId,
1672
+ name: 'RuleBuilder - Empty',
1673
+ component_name: 'RuleBuilder',
1674
+ component_path: 'app/components/RuleBuilder.tsx',
1675
+ entity_sha: 'sha-real',
1676
+ created_at: '2026-03-16 23:00:00',
1677
+ })
1678
+ .execute();
1679
+ // Phantom entity — scenario points to a SHA that doesn't exist
1680
+ // in the entities table (registered without component_path).
1681
+ // No entity record, no analyses, unfixable by analyze-imports.
1682
+ await db
1683
+ .insertInto('editor_scenarios')
1684
+ .values({
1685
+ id: 'sc-phantom',
1686
+ project_id: projectId,
1687
+ name: 'Empty',
1688
+ component_name: 'RuleBuilder',
1689
+ component_path: null,
1690
+ entity_sha: 'sha-phantom-no-entity-record',
1691
+ created_at: '2026-03-16 22:00:00',
1692
+ })
1693
+ .execute();
1694
+ const incomplete = await queryIncompleteEntities(db, projectId, null);
1695
+ // Should NOT report phantom SHAs as incomplete — they can't be fixed
1696
+ // by running analyze-imports (no entity record exists to resolve).
1697
+ expect(incomplete).toHaveLength(0);
1698
+ });
1699
+ it('should still report real incomplete entities (entity exists but no analysis)', async () => {
1700
+ // Real entity without analysis — this IS a legitimate incomplete entity
1701
+ await db
1702
+ .insertInto('entities')
1703
+ .values({
1704
+ sha: 'sha-noanalysis',
1705
+ name: 'Footer',
1706
+ entity_type: 'visual',
1707
+ file_path: 'app/components/Footer.tsx',
1708
+ })
1709
+ .execute();
1710
+ await db
1711
+ .insertInto('editor_scenarios')
1712
+ .values({
1713
+ id: 'sc-1',
1714
+ project_id: projectId,
1715
+ name: 'Footer - Default',
1716
+ component_name: 'Footer',
1717
+ component_path: 'app/components/Footer.tsx',
1718
+ entity_sha: 'sha-noanalysis',
1719
+ created_at: '2026-03-16 23:00:00',
1720
+ })
1721
+ .execute();
1722
+ // Phantom scenario (shouldn't affect results)
1723
+ await db
1724
+ .insertInto('editor_scenarios')
1725
+ .values({
1726
+ id: 'sc-phantom',
1727
+ project_id: projectId,
1728
+ name: 'Footer - Alt',
1729
+ component_name: 'Footer',
1730
+ component_path: null,
1731
+ entity_sha: 'sha-phantom-does-not-exist',
1732
+ created_at: '2026-03-16 22:00:00',
1733
+ })
1734
+ .execute();
1735
+ const incomplete = await queryIncompleteEntities(db, projectId, null);
1736
+ // Should report Footer (real entity, no analysis) but NOT the phantom
1737
+ expect(incomplete).toHaveLength(1);
1738
+ expect(incomplete[0].name).toBe('Footer');
1739
+ expect(incomplete[0].entitySha).toBe('sha-noanalysis');
1740
+ });
1741
+ });
1469
1742
  // ── queryMiscategorizedScenarios ─────────────────────────────────────
1470
1743
  describe('queryMiscategorizedScenarios', () => {
1471
1744
  let db;
@@ -1714,10 +1987,31 @@ describe('editorAudit', () => {
1714
1987
  allPassing: false,
1715
1988
  })).toBe(true);
1716
1989
  });
1990
+ it('should return false when there are also runner errors', () => {
1991
+ // functionsRunnerError means the test runner crashed — a real failure
1992
+ // that cannot be fixed by entity SHA backfill or analyze-imports.
1993
+ // If this returns true, checkAuditGate would attempt a useless backfill
1994
+ // instead of reporting the runner error, and isOnlyPreExistingIncomplete
1995
+ // could let the gate pass entirely.
1996
+ expect(isOnlyIncompleteEntities({
1997
+ componentsMissing: 0,
1998
+ componentsWithErrors: 0,
1999
+ functionsFailing: 0,
2000
+ functionsRunnerError: 2,
2001
+ functionsNameMismatch: 0,
2002
+ functionsMissing: 0,
2003
+ missingFromGlossary: 0,
2004
+ incompleteEntities: 1,
2005
+ allPassing: false,
2006
+ })).toBe(false);
2007
+ });
1717
2008
  });
1718
2009
  // ── isAutoRemediable ─────────────────────────────────────────────────
1719
2010
  describe('isAutoRemediable', () => {
1720
- it('should return true on first attempt when only incomplete entities', () => {
2011
+ // isAutoRemediable always returns false the audit never triggers
2012
+ // full analyze-imports inline. It takes minutes on large projects.
2013
+ // Only lightweight backfill is acceptable during audit.
2014
+ it('should return false even on first attempt with only incomplete entities', () => {
1721
2015
  const result = isAutoRemediable({
1722
2016
  componentsMissing: 0,
1723
2017
  componentsWithErrors: 0,
@@ -1729,11 +2023,9 @@ describe('editorAudit', () => {
1729
2023
  incompleteEntities: 3,
1730
2024
  allPassing: false,
1731
2025
  }, false);
1732
- expect(result).toBe(true);
2026
+ expect(result).toBe(false);
1733
2027
  });
1734
- it('should return false on second attempt (already tried once)', () => {
1735
- // This is the key fix: if we already tried analyze-imports and
1736
- // entities are STILL incomplete, don't try again — report the failure
2028
+ it('should return false on second attempt', () => {
1737
2029
  const result = isAutoRemediable({
1738
2030
  componentsMissing: 0,
1739
2031
  componentsWithErrors: 0,
@@ -2187,8 +2479,11 @@ describe('editorAudit', () => {
2187
2479
  },
2188
2480
  ]);
2189
2481
  });
2190
- it('should use entity name from entities table, falling back to component_name', async () => {
2191
- // Scenario has entity_sha but entity record doesn't exist
2482
+ it('should skip phantom SHAs (entity_sha with no entity record)', async () => {
2483
+ // Scenario has entity_sha but entity record doesn't exist.
2484
+ // These are "phantom SHAs" created when scenarios were registered
2485
+ // without component_path — they can never be fixed by analyze-imports
2486
+ // and should not block audit progression.
2192
2487
  await db
2193
2488
  .insertInto('editor_scenarios')
2194
2489
  .values({
@@ -2201,14 +2496,8 @@ describe('editorAudit', () => {
2201
2496
  })
2202
2497
  .execute();
2203
2498
  const result = await queryIncompleteEntities(db, projectId, null);
2204
- expect(result).toEqual([
2205
- {
2206
- entitySha: 'sha-ghost',
2207
- name: 'GhostComponent',
2208
- scenarioCount: 1,
2209
- preExisting: false,
2210
- },
2211
- ]);
2499
+ // Phantom SHAs are excluded — not reportable as incomplete
2500
+ expect(result).toEqual([]);
2212
2501
  });
2213
2502
  it('should detect incomplete entity whose scenario predates the session', async () => {
2214
2503
  // Entity with no analyses, scenario created BEFORE session
@@ -2508,6 +2797,25 @@ describe('editorAudit', () => {
2508
2797
  });
2509
2798
  expect(result).toHaveLength(0);
2510
2799
  });
2800
+ it('should NOT flag scenarios for new entities (they need creation, not recapture)', () => {
2801
+ // "new" entities are being seen for the first time. Their scenarios need
2802
+ // initial creation, not recapture of old screenshots. Flagging them as
2803
+ // "needs_recapture" sends the wrong remediation signal to Claude.
2804
+ const entityChangeStatus = {
2805
+ NewComponent: { status: 'new' },
2806
+ };
2807
+ const result = identifyScenariosNeedingRecapture({
2808
+ scenarios: [
2809
+ {
2810
+ name: 'NewComponent - Default',
2811
+ entityName: 'NewComponent',
2812
+ updatedInSession: false,
2813
+ },
2814
+ ],
2815
+ entityChangeStatus,
2816
+ });
2817
+ expect(result).toHaveLength(0);
2818
+ });
2511
2819
  });
2512
2820
  // ── detectDuplicateNames ──────────────────────────────────────────
2513
2821
  describe('detectDuplicateNames', () => {
@@ -2675,6 +2983,28 @@ describe('editorAudit', () => {
2675
2983
  expect(result.components[0].status).toBe('missing');
2676
2984
  expect(result.summary.componentsMissing).toBe(1);
2677
2985
  });
2986
+ it('should not count needs_recapture components as componentsOk', () => {
2987
+ // A needs_recapture component is not "ok" — it needs action. Counting it
2988
+ // in componentsOk is misleading: if totalComponents=2, componentsOk=2,
2989
+ // and componentsNeedingRecapture=1, the numbers don't add up (2+1 > 2).
2990
+ const result = computeAudit({
2991
+ components: [
2992
+ { name: 'Library', filePath: 'app/library/page.tsx' },
2993
+ { name: 'DrinkCard', filePath: 'app/components/DrinkCard.tsx' },
2994
+ ],
2995
+ functions: [],
2996
+ scenarioCounts: { DrinkCard: 2 },
2997
+ testFileExistence: {},
2998
+ totalScenarioCounts: { Library: 3 },
2999
+ entityChangeStatus: { Library: { status: 'impacted' } },
3000
+ });
3001
+ expect(result.components[0].status).toBe('needs_recapture');
3002
+ expect(result.components[1].status).toBe('ok');
3003
+ // needs_recapture is not "ok" — should be counted separately
3004
+ expect(result.summary.componentsOk).toBe(1);
3005
+ expect(result.summary.componentsNeedingRecapture).toBe(1);
3006
+ expect(result.summary.totalComponents).toBe(2);
3007
+ });
2678
3008
  });
2679
3009
  // ── queryUnassociatedScenarios ──────────────────────────────────────
2680
3010
  describe('queryUnassociatedScenarios', () => {
@@ -2923,29 +3253,659 @@ describe('editorAudit', () => {
2923
3253
  });
2924
3254
  });
2925
3255
  // ── isAutoRemediable with unassociatedScenarios ────────────────────
2926
- describe('isAutoRemediable with unassociatedScenarios', () => {
2927
- it('should return true when unassociatedScenarios is the only failure', () => {
2928
- expect(isAutoRemediable({ unassociatedScenarios: 3 }, false)).toBe(true);
3256
+ describe('isAutoRemediable always returns false (no inline full analysis)', () => {
3257
+ // Full analyze-imports takes minutes on large projects. The audit should
3258
+ // never trigger it — only the lightweight backfill path is acceptable.
3259
+ it('should return false for unassociatedScenarios only', () => {
3260
+ expect(isAutoRemediable({ unassociatedScenarios: 3 }, false)).toBe(false);
2929
3261
  });
2930
- it('should return true when both incompleteEntities and unassociatedScenarios are the only failures', () => {
2931
- expect(isAutoRemediable({ incompleteEntities: 1, unassociatedScenarios: 2 }, false)).toBe(true);
3262
+ it('should return false for incompleteEntities + unassociatedScenarios', () => {
3263
+ expect(isAutoRemediable({ incompleteEntities: 1, unassociatedScenarios: 2 }, false)).toBe(false);
2932
3264
  });
2933
- it('should return false when unassociatedScenarios exist alongside other failures', () => {
2934
- expect(isAutoRemediable({ unassociatedScenarios: 2, componentsMissing: 1 }, false)).toBe(false);
3265
+ it('should return false even with no other failures', () => {
3266
+ expect(isAutoRemediable({ unassociatedScenarios: 2 }, false)).toBe(false);
2935
3267
  });
2936
3268
  it('should return false when already attempted', () => {
2937
3269
  expect(isAutoRemediable({ unassociatedScenarios: 3 }, true)).toBe(false);
2938
3270
  });
2939
3271
  });
2940
- describe('isOnlyIncompleteEntities with unassociatedScenarios', () => {
2941
- it('should return true when only unassociatedScenarios present', () => {
2942
- expect(isOnlyIncompleteEntities({ unassociatedScenarios: 5 })).toBe(true);
3272
+ describe('suggestedTestFile for functions without testFile', () => {
3273
+ it('should suggest conventional .test.ts path when testFile is undefined', () => {
3274
+ const result = computeAudit({
3275
+ components: [],
3276
+ functions: [
3277
+ { name: 'useLibraryShell', filePath: 'app/library/context.tsx' },
3278
+ ],
3279
+ scenarioCounts: {},
3280
+ testFileExistence: {},
3281
+ });
3282
+ expect(result.functions[0].suggestedTestFile).toBe('app/library/context.test.ts');
3283
+ expect(result.functions[0].status).toBe('missing');
2943
3284
  });
2944
- it('should return false when unassociatedScenarios present with other failures', () => {
2945
- expect(isOnlyIncompleteEntities({
2946
- unassociatedScenarios: 5,
2947
- functionsMissing: 1,
2948
- })).toBe(false);
3285
+ it('should suggest .test.ts for .ts files', () => {
3286
+ const result = computeAudit({
3287
+ components: [],
3288
+ functions: [{ name: 'calculatePrice', filePath: 'app/lib/pricing.ts' }],
3289
+ scenarioCounts: {},
3290
+ testFileExistence: {},
3291
+ });
3292
+ expect(result.functions[0].suggestedTestFile).toBe('app/lib/pricing.test.ts');
3293
+ });
3294
+ it('should not set suggestedTestFile when testFile is already specified', () => {
3295
+ const result = computeAudit({
3296
+ components: [],
3297
+ functions: [
3298
+ {
3299
+ name: 'calculatePrice',
3300
+ filePath: 'app/lib/pricing.ts',
3301
+ testFile: 'app/lib/pricing.test.ts',
3302
+ },
3303
+ ],
3304
+ scenarioCounts: {},
3305
+ testFileExistence: { 'app/lib/pricing.test.ts': true },
3306
+ });
3307
+ expect(result.functions[0].suggestedTestFile).toBeUndefined();
3308
+ });
3309
+ });
3310
+ describe('hint for function audit entries', () => {
3311
+ it('should include a hint for name_mismatch functions explaining the fix', () => {
3312
+ // Claude sees "name mismatch" with no guidance on what it means or how
3313
+ // to fix it. The hint should explain that a top-level describe block
3314
+ // matching the function name is required for the CodeYam UI.
3315
+ const result = computeAudit({
3316
+ components: [],
3317
+ functions: [
3318
+ {
3319
+ name: 'useDrinks',
3320
+ filePath: 'app/hooks/useDrinks.ts',
3321
+ testFile: 'app/hooks/useDrinks.test.ts',
3322
+ },
3323
+ ],
3324
+ scenarioCounts: {},
3325
+ testFileExistence: { 'app/hooks/useDrinks.test.ts': true },
3326
+ testResults: {
3327
+ 'app/hooks/useDrinks.test.ts': {
3328
+ passing: true,
3329
+ hasEntityNameDescribe: false,
3330
+ },
3331
+ },
3332
+ });
3333
+ expect(result.functions[0].status).toBe('name_mismatch');
3334
+ expect(result.functions[0].hint).toBeDefined();
3335
+ expect(result.functions[0].hint).toContain('describe');
3336
+ expect(result.functions[0].hint).toContain('useDrinks');
3337
+ });
3338
+ it('should include a hint for runner_error functions showing the error', () => {
3339
+ // When the test runner crashes, Claude needs to see WHY it crashed
3340
+ // to fix the underlying issue. Without this, Claude loops re-running audit.
3341
+ const result = computeAudit({
3342
+ components: [],
3343
+ functions: [
3344
+ {
3345
+ name: 'getTimeAgo',
3346
+ filePath: 'src/lib/format.ts',
3347
+ testFile: 'src/lib/format.test.ts',
3348
+ },
3349
+ ],
3350
+ scenarioCounts: {},
3351
+ testFileExistence: { 'src/lib/format.test.ts': true },
3352
+ testResults: {
3353
+ 'src/lib/format.test.ts': {
3354
+ passing: false,
3355
+ hasEntityNameDescribe: false,
3356
+ errorMessage: 'Cannot find module "@/lib/format"',
3357
+ },
3358
+ },
3359
+ });
3360
+ expect(result.functions[0].status).toBe('runner_error');
3361
+ expect(result.functions[0].hint).toBeDefined();
3362
+ expect(result.functions[0].hint).toContain('Cannot find module');
3363
+ });
3364
+ });
3365
+ describe('hint for missing components', () => {
3366
+ it('should hint that layout files need app-level scenarios', () => {
3367
+ const result = computeAudit({
3368
+ components: [
3369
+ { name: 'LibraryLayout', filePath: 'app/library/layout.tsx' },
3370
+ ],
3371
+ functions: [],
3372
+ scenarioCounts: {},
3373
+ testFileExistence: {},
3374
+ });
3375
+ expect(result.components[0].hint).toContain('layout');
3376
+ expect(result.components[0].hint).toContain('pageFilePath');
3377
+ });
3378
+ it('should hint that page files need app-level scenarios', () => {
3379
+ const result = computeAudit({
3380
+ components: [
3381
+ { name: 'InboxPage', filePath: 'app/library/inbox/page.tsx' },
3382
+ ],
3383
+ functions: [],
3384
+ scenarioCounts: {},
3385
+ testFileExistence: {},
3386
+ });
3387
+ expect(result.components[0].hint).toContain('page');
3388
+ expect(result.components[0].hint).toContain('pageFilePath');
3389
+ });
3390
+ it('should hint that regular components need isolation routes', () => {
3391
+ const result = computeAudit({
3392
+ components: [
3393
+ { name: 'DrinkCard', filePath: 'app/components/DrinkCard.tsx' },
3394
+ ],
3395
+ functions: [],
3396
+ scenarioCounts: {},
3397
+ testFileExistence: {},
3398
+ });
3399
+ expect(result.components[0].hint).toContain('isolated-components');
3400
+ });
3401
+ it('should not set hint when component has scenarios', () => {
3402
+ const result = computeAudit({
3403
+ components: [
3404
+ { name: 'DrinkCard', filePath: 'app/components/DrinkCard.tsx' },
3405
+ ],
3406
+ functions: [],
3407
+ scenarioCounts: { DrinkCard: 2 },
3408
+ testFileExistence: {},
3409
+ });
3410
+ expect(result.components[0].hint).toBeUndefined();
3411
+ });
3412
+ it('should provide a hint for needs_recapture components', () => {
3413
+ // Components with needs_recapture status need guidance on what to do.
3414
+ // Without a hint, Claude has no instructions for fixing the issue.
3415
+ const result = computeAudit({
3416
+ components: [{ name: 'Library', filePath: 'app/library/page.tsx' }],
3417
+ functions: [],
3418
+ scenarioCounts: {},
3419
+ testFileExistence: {},
3420
+ totalScenarioCounts: { Library: 3 },
3421
+ entityChangeStatus: { Library: { status: 'impacted' } },
3422
+ });
3423
+ expect(result.components[0].status).toBe('needs_recapture');
3424
+ expect(result.components[0].hint).toBeDefined();
3425
+ expect(result.components[0].hint).toContain('recapture');
3426
+ });
3427
+ });
3428
+ describe('formatIncompleteEntityGuidance', () => {
3429
+ it('should include the entity name and scenario count', () => {
3430
+ const { formatIncompleteEntityGuidance } = require('../editorAudit');
3431
+ const result = formatIncompleteEntityGuidance({
3432
+ entitySha: 'abc123',
3433
+ name: 'RuleBuilder',
3434
+ scenarioCount: 5,
3435
+ preExisting: false,
3436
+ });
3437
+ expect(result).toContain('RuleBuilder');
3438
+ expect(result).toContain('5');
3439
+ });
3440
+ it('should tell Claude the exact fix command', () => {
3441
+ const { formatIncompleteEntityGuidance } = require('../editorAudit');
3442
+ const result = formatIncompleteEntityGuidance({
3443
+ entitySha: 'abc123',
3444
+ name: 'RuleBuilder',
3445
+ scenarioCount: 5,
3446
+ preExisting: false,
3447
+ });
3448
+ expect(result).toContain('codeyam editor analyze-imports');
3449
+ });
3450
+ it('should flag pre-existing issues as non-blocking', () => {
3451
+ const { formatIncompleteEntityGuidance } = require('../editorAudit');
3452
+ const result = formatIncompleteEntityGuidance({
3453
+ entitySha: 'abc123',
3454
+ name: 'RuleBuilder',
3455
+ scenarioCount: 5,
3456
+ preExisting: true,
3457
+ });
3458
+ expect(result).toContain('pre-existing');
3459
+ });
3460
+ it('should explain what incomplete means', () => {
3461
+ const { formatIncompleteEntityGuidance } = require('../editorAudit');
3462
+ const result = formatIncompleteEntityGuidance({
3463
+ entitySha: 'abc123',
3464
+ name: 'RuleBuilder',
3465
+ scenarioCount: 5,
3466
+ preExisting: false,
3467
+ });
3468
+ // Should explain the root cause, not just the symptom
3469
+ expect(result).toMatch(/scenario.*without.*import graph|import graph.*not.*built/i);
3470
+ });
3471
+ });
3472
+ describe('formatManualAnalysisGuidance', () => {
3473
+ it('should include entity name, scenario count, and error message', () => {
3474
+ const { formatManualAnalysisGuidance } = require('../editorAudit');
3475
+ const result = formatManualAnalysisGuidance({
3476
+ name: 'Header',
3477
+ filePath: 'src/components/Header.tsx',
3478
+ scenarioCount: 3,
3479
+ error: 'TypeScript parsing error: unexpected token',
3480
+ });
3481
+ expect(result).toContain('Header');
3482
+ expect(result).toContain('3 scenario(s)');
3483
+ expect(result).toContain('TypeScript parsing error');
3484
+ });
3485
+ it('should include MANUAL ANALYSIS REQUIRED header', () => {
3486
+ const { formatManualAnalysisGuidance } = require('../editorAudit');
3487
+ const result = formatManualAnalysisGuidance({
3488
+ name: 'Header',
3489
+ filePath: 'src/components/Header.tsx',
3490
+ scenarioCount: 2,
3491
+ error: 'Parse error',
3492
+ });
3493
+ expect(result).toContain('MANUAL ANALYSIS REQUIRED');
3494
+ });
3495
+ it('should tell Claude to read the source file', () => {
3496
+ const { formatManualAnalysisGuidance } = require('../editorAudit');
3497
+ const result = formatManualAnalysisGuidance({
3498
+ name: 'Header',
3499
+ filePath: 'src/components/Header.tsx',
3500
+ scenarioCount: 2,
3501
+ error: 'Parse error',
3502
+ });
3503
+ expect(result).toContain('Read src/components/Header.tsx');
3504
+ });
3505
+ it('should include the manual-entity command', () => {
3506
+ const { formatManualAnalysisGuidance } = require('../editorAudit');
3507
+ const result = formatManualAnalysisGuidance({
3508
+ name: 'Header',
3509
+ filePath: 'src/components/Header.tsx',
3510
+ scenarioCount: 2,
3511
+ error: 'Parse error',
3512
+ });
3513
+ expect(result).toContain('codeyam editor manual-entity');
3514
+ });
3515
+ it('should tell Claude to check glossary for imports', () => {
3516
+ const { formatManualAnalysisGuidance } = require('../editorAudit');
3517
+ const result = formatManualAnalysisGuidance({
3518
+ name: 'Header',
3519
+ filePath: 'src/components/Header.tsx',
3520
+ scenarioCount: 2,
3521
+ error: 'Parse error',
3522
+ });
3523
+ expect(result).toContain('glossary');
3524
+ });
3525
+ });
3526
+ describe('getIncompleteEntityFilePaths', () => {
3527
+ // The audit should auto-fix incomplete entities by running analysis on
3528
+ // just their specific file paths, not all 117+ files. This function
3529
+ // resolves entity SHAs to file paths for targeted analysis.
3530
+ let db;
3531
+ let rawDb;
3532
+ beforeEach(async () => {
3533
+ rawDb = new Database(':memory:');
3534
+ db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
3535
+ await db.schema
3536
+ .createTable('entities')
3537
+ .addColumn('sha', 'varchar', (col) => col.primaryKey())
3538
+ .addColumn('name', 'varchar')
3539
+ .addColumn('file_path', 'varchar')
3540
+ .addColumn('project_id', 'varchar')
3541
+ .addColumn('entity_type', 'varchar')
3542
+ .execute();
3543
+ });
3544
+ afterEach(() => {
3545
+ rawDb.close();
3546
+ });
3547
+ it('should resolve entity SHAs to file paths from the entities table', async () => {
3548
+ const { getIncompleteEntityFilePaths } = require('../editorAudit');
3549
+ await db
3550
+ .insertInto('entities')
3551
+ .values([
3552
+ {
3553
+ sha: 'sha-rule',
3554
+ name: 'RuleBuilder',
3555
+ file_path: 'app/components/RuleBuilder.tsx',
3556
+ project_id: 'p1',
3557
+ entity_type: 'component',
3558
+ },
3559
+ {
3560
+ sha: 'sha-row',
3561
+ name: 'ArticleTableRow',
3562
+ file_path: 'app/components/ArticleTableRow.tsx',
3563
+ project_id: 'p1',
3564
+ entity_type: 'component',
3565
+ },
3566
+ ])
3567
+ .execute();
3568
+ const result = await getIncompleteEntityFilePaths(db, [
3569
+ {
3570
+ entitySha: 'sha-rule',
3571
+ name: 'RuleBuilder',
3572
+ scenarioCount: 5,
3573
+ preExisting: false,
3574
+ },
3575
+ {
3576
+ entitySha: 'sha-row',
3577
+ name: 'ArticleTableRow',
3578
+ scenarioCount: 2,
3579
+ preExisting: false,
3580
+ },
3581
+ ]);
3582
+ expect(result).toContain('app/components/RuleBuilder.tsx');
3583
+ expect(result).toContain('app/components/ArticleTableRow.tsx');
3584
+ expect(result).toHaveLength(2);
3585
+ });
3586
+ it('should skip entities whose SHA is not in the entities table', async () => {
3587
+ const { getIncompleteEntityFilePaths } = require('../editorAudit');
3588
+ const result = await getIncompleteEntityFilePaths(db, [
3589
+ {
3590
+ entitySha: 'nonexistent-sha',
3591
+ name: 'Ghost',
3592
+ scenarioCount: 1,
3593
+ preExisting: false,
3594
+ },
3595
+ ]);
3596
+ expect(result).toHaveLength(0);
3597
+ });
3598
+ it('should deduplicate file paths', async () => {
3599
+ const { getIncompleteEntityFilePaths } = require('../editorAudit');
3600
+ await db
3601
+ .insertInto('entities')
3602
+ .values([
3603
+ {
3604
+ sha: 'sha-v1',
3605
+ name: 'Foo',
3606
+ file_path: 'app/Foo.tsx',
3607
+ project_id: 'p1',
3608
+ entity_type: 'component',
3609
+ },
3610
+ {
3611
+ sha: 'sha-v2',
3612
+ name: 'Foo',
3613
+ file_path: 'app/Foo.tsx',
3614
+ project_id: 'p1',
3615
+ entity_type: 'component',
3616
+ },
3617
+ ])
3618
+ .execute();
3619
+ const result = await getIncompleteEntityFilePaths(db, [
3620
+ {
3621
+ entitySha: 'sha-v1',
3622
+ name: 'Foo',
3623
+ scenarioCount: 3,
3624
+ preExisting: false,
3625
+ },
3626
+ {
3627
+ entitySha: 'sha-v2',
3628
+ name: 'Foo',
3629
+ scenarioCount: 1,
3630
+ preExisting: false,
3631
+ },
3632
+ ]);
3633
+ expect(result).toEqual(['app/Foo.tsx']);
3634
+ });
3635
+ });
3636
+ describe('isAutoRemediable never triggers full analysis', () => {
3637
+ // The audit must NEVER run handleAnalyzeImports inline — it takes minutes
3638
+ // for large projects. Auto-remediation should only do the lightweight
3639
+ // entity SHA backfill. isAutoRemediable is now always false; the callers
3640
+ // use needsBackfillOnly for the fast path instead.
3641
+ it('should always return false regardless of summary state', () => {
3642
+ expect(isAutoRemediable({ incompleteEntities: 5 }, false)).toBe(false);
3643
+ expect(isAutoRemediable({ unassociatedScenarios: 3 }, false)).toBe(false);
3644
+ expect(isAutoRemediable({ incompleteEntities: 1, unassociatedScenarios: 2 }, false)).toBe(false);
3645
+ });
3646
+ });
3647
+ // ── isOnlyPreExistingIncomplete ─────────────────────────────────────
3648
+ describe('isOnlyPreExistingIncomplete', () => {
3649
+ it('should return true when all incomplete entities are pre-existing and no other failures', () => {
3650
+ expect(isOnlyPreExistingIncomplete({
3651
+ incompleteEntities: 2,
3652
+ preExistingIncompleteEntities: 2,
3653
+ componentsMissing: 0,
3654
+ componentsWithErrors: 0,
3655
+ functionsFailing: 0,
3656
+ functionsNameMismatch: 0,
3657
+ functionsMissing: 0,
3658
+ missingFromGlossary: 0,
3659
+ miscategorizedScenarios: 0,
3660
+ scenariosNeedingRecapture: 0,
3661
+ }, [
3662
+ {
3663
+ entitySha: 'a',
3664
+ name: 'RuleBuilder',
3665
+ scenarioCount: 5,
3666
+ preExisting: true,
3667
+ },
3668
+ {
3669
+ entitySha: 'b',
3670
+ name: 'ArticleTableRow',
3671
+ scenarioCount: 2,
3672
+ preExisting: true,
3673
+ },
3674
+ ])).toBe(true);
3675
+ });
3676
+ it('should return false when there are runner errors even if all incompletes are pre-existing', () => {
3677
+ // Safety net: runner errors (crashed test runner) must NEVER bypass the gate.
3678
+ // Without this test, a regression removing functionsRunnerError from
3679
+ // isOnlyIncompleteEntities would silently let broken test infrastructure through.
3680
+ expect(isOnlyPreExistingIncomplete({
3681
+ incompleteEntities: 1,
3682
+ preExistingIncompleteEntities: 1,
3683
+ componentsMissing: 0,
3684
+ componentsWithErrors: 0,
3685
+ functionsFailing: 0,
3686
+ functionsRunnerError: 1,
3687
+ functionsNameMismatch: 0,
3688
+ functionsMissing: 0,
3689
+ missingFromGlossary: 0,
3690
+ }, [
3691
+ {
3692
+ entitySha: 'a',
3693
+ name: 'OldComponent',
3694
+ scenarioCount: 1,
3695
+ preExisting: true,
3696
+ },
3697
+ ])).toBe(false);
3698
+ });
3699
+ it('should return false when some incomplete entities are NOT pre-existing', () => {
3700
+ expect(isOnlyPreExistingIncomplete({
3701
+ incompleteEntities: 2,
3702
+ preExistingIncompleteEntities: 1,
3703
+ componentsMissing: 0,
3704
+ componentsWithErrors: 0,
3705
+ functionsFailing: 0,
3706
+ functionsNameMismatch: 0,
3707
+ functionsMissing: 0,
3708
+ missingFromGlossary: 0,
3709
+ miscategorizedScenarios: 0,
3710
+ scenariosNeedingRecapture: 0,
3711
+ }, [
3712
+ {
3713
+ entitySha: 'a',
3714
+ name: 'RuleBuilder',
3715
+ scenarioCount: 5,
3716
+ preExisting: true,
3717
+ },
3718
+ {
3719
+ entitySha: 'b',
3720
+ name: 'NewComponent',
3721
+ scenarioCount: 1,
3722
+ preExisting: false,
3723
+ },
3724
+ ])).toBe(false);
3725
+ });
3726
+ it('should return false when there are other failures besides incomplete entities', () => {
3727
+ expect(isOnlyPreExistingIncomplete({
3728
+ incompleteEntities: 2,
3729
+ preExistingIncompleteEntities: 2,
3730
+ componentsMissing: 1,
3731
+ }, [
3732
+ {
3733
+ entitySha: 'a',
3734
+ name: 'RuleBuilder',
3735
+ scenarioCount: 5,
3736
+ preExisting: true,
3737
+ },
3738
+ {
3739
+ entitySha: 'b',
3740
+ name: 'ArticleTableRow',
3741
+ scenarioCount: 2,
3742
+ preExisting: true,
3743
+ },
3744
+ ])).toBe(false);
3745
+ });
3746
+ it('should return false when incomplete entities array is empty', () => {
3747
+ expect(isOnlyPreExistingIncomplete({ incompleteEntities: 0 }, [])).toBe(false);
3748
+ });
3749
+ it('should return false when incomplete entities array is missing', () => {
3750
+ expect(isOnlyPreExistingIncomplete({ incompleteEntities: 2, preExistingIncompleteEntities: 2 }, undefined)).toBe(false);
3751
+ });
3752
+ });
3753
+ describe('isOnlyIncompleteEntities with unassociatedScenarios', () => {
3754
+ it('should return true when only unassociatedScenarios present', () => {
3755
+ expect(isOnlyIncompleteEntities({ unassociatedScenarios: 5 })).toBe(true);
3756
+ });
3757
+ it('should return false when unassociatedScenarios present with other failures', () => {
3758
+ expect(isOnlyIncompleteEntities({
3759
+ unassociatedScenarios: 5,
3760
+ functionsMissing: 1,
3761
+ })).toBe(false);
3762
+ });
3763
+ });
3764
+ // ── aggregateClientErrorsByComponent ─────────────────────────────────
3765
+ describe('aggregateClientErrorsByComponent', () => {
3766
+ it('should attribute errors to component using componentName from metadata', () => {
3767
+ const result = aggregateClientErrorsByComponent({
3768
+ 'scenario-1': {
3769
+ scenarioName: 'DrinkCard - Default',
3770
+ errors: ['TypeError: Cannot read property "price"'],
3771
+ },
3772
+ }, [
3773
+ {
3774
+ name: 'DrinkCard - Default',
3775
+ componentName: 'DrinkCard',
3776
+ },
3777
+ ]);
3778
+ expect(result['DrinkCard']).toEqual([
3779
+ 'TypeError: Cannot read property "price"',
3780
+ ]);
3781
+ });
3782
+ it('should attribute app-level scenario errors using pageFilePath', () => {
3783
+ // App-level scenarios have componentName=null. The old approach
3784
+ // parsed the scenario name "Full Page with Library" and got
3785
+ // "Full Page with Library" as the component — which matches nothing.
3786
+ // With metadata, we derive the entity name from pageFilePath.
3787
+ const result = aggregateClientErrorsByComponent({
3788
+ 'scenario-1': {
3789
+ scenarioName: 'Full Page with Library',
3790
+ errors: ['TypeError: Cannot read property "title"'],
3791
+ },
3792
+ }, [
3793
+ {
3794
+ name: 'Full Page with Library',
3795
+ componentName: null,
3796
+ pageFilePath: 'app/library/page.tsx',
3797
+ },
3798
+ ]);
3799
+ // Should be keyed by the canonical entity name from scenarioEntityName()
3800
+ expect(result['Library']).toEqual([
3801
+ 'TypeError: Cannot read property "title"',
3802
+ ]);
3803
+ });
3804
+ it('should fall back to scenario name parsing when no metadata match exists', () => {
3805
+ // If the scenario is not in the metadata list (e.g., metadata not yet loaded),
3806
+ // fall back to the "ComponentName - Variant" convention
3807
+ const result = aggregateClientErrorsByComponent({
3808
+ 'scenario-1': {
3809
+ scenarioName: 'Header - Dark Mode',
3810
+ errors: ['ReferenceError: theme is not defined'],
3811
+ },
3812
+ }, []);
3813
+ expect(result['Header']).toEqual([
3814
+ 'ReferenceError: theme is not defined',
3815
+ ]);
3816
+ });
3817
+ it('should handle non-route file paths via scenarioEntityName', () => {
3818
+ // Non-route files (src/...) are treated as app entry points by
3819
+ // scenarioEntityName → buildRoutePattern returns '/' → 'Home'.
3820
+ // When a url is available, it provides a better entity name.
3821
+ const result = aggregateClientErrorsByComponent({
3822
+ 'scenario-1': {
3823
+ scenarioName: 'LibraryApp - Rich',
3824
+ errors: ['Error: fetch failed'],
3825
+ },
3826
+ }, [
3827
+ {
3828
+ name: 'LibraryApp - Rich',
3829
+ componentName: null,
3830
+ pageFilePath: 'src/library/LibraryApp.tsx',
3831
+ url: '/library',
3832
+ },
3833
+ ]);
3834
+ // scenarioEntityName uses url fallback when pageFilePath is a non-route file
3835
+ // pageFilePath 'src/...' → buildRoutePattern → '/' → routeDisplayName → 'Home'
3836
+ // But componentName takes priority, and with url '/library' as final fallback
3837
+ // scenarioEntityName({ pageFilePath: 'src/library/LibraryApp.tsx' }) → 'Home'
3838
+ // because pageFilePath is checked before url
3839
+ expect(result['Home']).toEqual(['Error: fetch failed']);
3840
+ });
3841
+ it('should skip scenarios with no errors', () => {
3842
+ const result = aggregateClientErrorsByComponent({
3843
+ 'scenario-1': {
3844
+ scenarioName: 'DrinkCard - Default',
3845
+ errors: [],
3846
+ },
3847
+ }, [
3848
+ {
3849
+ name: 'DrinkCard - Default',
3850
+ componentName: 'DrinkCard',
3851
+ },
3852
+ ]);
3853
+ expect(result).toEqual({});
3854
+ });
3855
+ it('should aggregate errors from multiple scenarios for same component', () => {
3856
+ const result = aggregateClientErrorsByComponent({
3857
+ 'scenario-1': {
3858
+ scenarioName: 'DrinkCard - Default',
3859
+ errors: ['Error A'],
3860
+ },
3861
+ 'scenario-2': {
3862
+ scenarioName: 'DrinkCard - Hover',
3863
+ errors: ['Error B', 'Error C'],
3864
+ },
3865
+ }, [
3866
+ { name: 'DrinkCard - Default', componentName: 'DrinkCard' },
3867
+ { name: 'DrinkCard - Hover', componentName: 'DrinkCard' },
3868
+ ]);
3869
+ expect(result['DrinkCard']).toEqual(['Error A', 'Error B', 'Error C']);
3870
+ });
3871
+ it('should use capitalized display name for page file paths, not lowercase directory', () => {
3872
+ // aggregateClientErrorsByComponent must produce keys matching scenarioEntityName().
3873
+ // scenarioEntityName({ pageFilePath: 'app/library/page.tsx' }) returns 'Library',
3874
+ // so the key must be 'Library' — not 'library' (the raw directory name).
3875
+ // computeAudit checks clientErrors[glossaryEntryName], so a lowercase key
3876
+ // will never match, silently dropping all client errors for page-level scenarios.
3877
+ const result = aggregateClientErrorsByComponent({
3878
+ 'sc-1': {
3879
+ scenarioName: 'Library - Default',
3880
+ errors: ['TypeError: fetch failed'],
3881
+ },
3882
+ }, [
3883
+ {
3884
+ name: 'Library - Default',
3885
+ componentName: null,
3886
+ pageFilePath: 'app/library/page.tsx',
3887
+ },
3888
+ ]);
3889
+ expect(result).toHaveProperty('Library');
3890
+ expect(result['Library']).toEqual(['TypeError: fetch failed']);
3891
+ });
3892
+ it('should use "Home" for root page app/page.tsx, not "app"', () => {
3893
+ // Root page: scenarioEntityName returns 'Home', not 'app'.
3894
+ // Custom path parsing incorrectly pops the parent dir ('app').
3895
+ const result = aggregateClientErrorsByComponent({
3896
+ 'sc-1': {
3897
+ scenarioName: 'Home - Default',
3898
+ errors: ['ReferenceError: window is not defined'],
3899
+ },
3900
+ }, [
3901
+ {
3902
+ name: 'Home - Default',
3903
+ componentName: null,
3904
+ pageFilePath: 'app/page.tsx',
3905
+ },
3906
+ ]);
3907
+ expect(result).toHaveProperty('Home');
3908
+ expect(result['Home']).toEqual(['ReferenceError: window is not defined']);
2949
3909
  });
2950
3910
  });
2951
3911
  });