@codeyam/codeyam-cli 0.1.19 → 0.1.21

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 (107) 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 +2 -2
  4. package/analyzer-template/packages/ai/src/lib/dataStructure/ScopeDataStructure.ts +5 -1
  5. package/analyzer-template/packages/analyze/src/lib/files/analyze/findOrCreateEntity.ts +10 -6
  6. package/analyzer-template/packages/analyze/src/lib/files/analyze/gatherEntityMap.ts +9 -12
  7. package/analyzer-template/packages/analyze/src/lib/files/analyzeChange.ts +4 -0
  8. package/analyzer-template/packages/analyze/src/lib/files/analyzeInitial.ts +4 -0
  9. package/analyzer-template/packages/aws/package.json +1 -1
  10. package/analyzer-template/packages/database/package.json +1 -1
  11. package/analyzer-template/packages/database/src/lib/loadAnalysis.ts +19 -15
  12. package/analyzer-template/packages/database/src/lib/loadEntity.ts +19 -8
  13. package/analyzer-template/packages/github/dist/database/src/lib/loadAnalysis.d.ts.map +1 -1
  14. package/analyzer-template/packages/github/dist/database/src/lib/loadAnalysis.js +1 -1
  15. package/analyzer-template/packages/github/dist/database/src/lib/loadAnalysis.js.map +1 -1
  16. package/analyzer-template/packages/github/dist/database/src/lib/loadEntity.d.ts +4 -1
  17. package/analyzer-template/packages/github/dist/database/src/lib/loadEntity.d.ts.map +1 -1
  18. package/analyzer-template/packages/github/dist/database/src/lib/loadEntity.js +5 -5
  19. package/analyzer-template/packages/github/dist/database/src/lib/loadEntity.js.map +1 -1
  20. package/analyzer-template/packages/utils/dist/utils/src/lib/fs/rsyncCopy.d.ts +3 -1
  21. package/analyzer-template/packages/utils/dist/utils/src/lib/fs/rsyncCopy.d.ts.map +1 -1
  22. package/analyzer-template/packages/utils/dist/utils/src/lib/fs/rsyncCopy.js +22 -1
  23. package/analyzer-template/packages/utils/dist/utils/src/lib/fs/rsyncCopy.js.map +1 -1
  24. package/analyzer-template/packages/utils/src/lib/fs/rsyncCopy.ts +27 -0
  25. package/analyzer-template/project/analyzeFileEntities.ts +26 -0
  26. package/background/src/lib/virtualized/project/analyzeFileEntities.js +22 -0
  27. package/background/src/lib/virtualized/project/analyzeFileEntities.js.map +1 -1
  28. package/codeyam-cli/src/commands/editor.js +412 -96
  29. package/codeyam-cli/src/commands/editor.js.map +1 -1
  30. package/codeyam-cli/src/utils/__tests__/editorAudit.test.js +895 -16
  31. package/codeyam-cli/src/utils/__tests__/editorAudit.test.js.map +1 -1
  32. package/codeyam-cli/src/utils/__tests__/editorScenarios.test.js +20 -0
  33. package/codeyam-cli/src/utils/__tests__/editorScenarios.test.js.map +1 -1
  34. package/codeyam-cli/src/utils/__tests__/registerScenarioResult.test.js +127 -0
  35. package/codeyam-cli/src/utils/__tests__/registerScenarioResult.test.js.map +1 -0
  36. package/codeyam-cli/src/utils/__tests__/screenshotHash.test.js +84 -0
  37. package/codeyam-cli/src/utils/__tests__/screenshotHash.test.js.map +1 -0
  38. package/codeyam-cli/src/utils/analysisRunner.js +8 -6
  39. package/codeyam-cli/src/utils/analysisRunner.js.map +1 -1
  40. package/codeyam-cli/src/utils/analyzer.js +8 -0
  41. package/codeyam-cli/src/utils/analyzer.js.map +1 -1
  42. package/codeyam-cli/src/utils/editorAudit.js +183 -13
  43. package/codeyam-cli/src/utils/editorAudit.js.map +1 -1
  44. package/codeyam-cli/src/utils/editorScenarios.js +36 -4
  45. package/codeyam-cli/src/utils/editorScenarios.js.map +1 -1
  46. package/codeyam-cli/src/utils/queue/job.js +6 -3
  47. package/codeyam-cli/src/utils/queue/job.js.map +1 -1
  48. package/codeyam-cli/src/utils/registerScenarioResult.js +52 -0
  49. package/codeyam-cli/src/utils/registerScenarioResult.js.map +1 -0
  50. package/codeyam-cli/src/utils/screenshotHash.js +26 -0
  51. package/codeyam-cli/src/utils/screenshotHash.js.map +1 -0
  52. package/codeyam-cli/src/webserver/__tests__/clientErrors.test.js +28 -1
  53. package/codeyam-cli/src/webserver/__tests__/clientErrors.test.js.map +1 -1
  54. package/codeyam-cli/src/webserver/__tests__/idleDetector.test.js +22 -2
  55. package/codeyam-cli/src/webserver/__tests__/idleDetector.test.js.map +1 -1
  56. package/codeyam-cli/src/webserver/__tests__/stripClaudeCommand.test.js +57 -1
  57. package/codeyam-cli/src/webserver/__tests__/stripClaudeCommand.test.js.map +1 -1
  58. package/codeyam-cli/src/webserver/app/lib/clientErrors.js +15 -0
  59. package/codeyam-cli/src/webserver/app/lib/clientErrors.js.map +1 -1
  60. package/codeyam-cli/src/webserver/build/client/assets/api.editor-save-scenario-data-l0sNRNKZ.js +1 -0
  61. package/codeyam-cli/src/webserver/build/client/assets/api.editor-schema-l0sNRNKZ.js +1 -0
  62. package/codeyam-cli/src/webserver/build/client/assets/{cy-logo-cli-CCKUIm0S.svg → cy-logo-cli-CJzc4vOH.svg} +2 -2
  63. package/codeyam-cli/src/webserver/build/client/assets/cy-logo-cli-DODLxLcw.js +1 -0
  64. package/codeyam-cli/src/webserver/build/client/assets/editor.entity.(_sha)-Dx-h1rJK.js +130 -0
  65. package/codeyam-cli/src/webserver/build/client/assets/editorPreview-NTuLi4Xg.js +41 -0
  66. package/codeyam-cli/src/webserver/build/client/assets/{entity._sha.scenarios._scenarioId.dev-KTQuL0aj.js → entity._sha.scenarios._scenarioId.dev-BA5L8bU-.js} +1 -1
  67. package/codeyam-cli/src/webserver/build/client/assets/{entity._sha.scenarios._scenarioId.fullscreen-C6eeL24i.js → entity._sha.scenarios._scenarioId.fullscreen-D4dmRgvO.js} +1 -1
  68. package/codeyam-cli/src/webserver/build/client/assets/globals-BrPXT1iR.css +1 -0
  69. package/codeyam-cli/src/webserver/build/client/assets/manifest-5025e428.js +1 -0
  70. package/codeyam-cli/src/webserver/build/client/assets/{root-BxUQigda.js → root-BCx1S8Z3.js} +26 -13
  71. package/codeyam-cli/src/webserver/build/server/assets/analysisRunner-C1kjC9UJ.js +13 -0
  72. package/codeyam-cli/src/webserver/build/server/assets/{index-Cd-ufawF.js → index-C91yWWCI.js} +1 -1
  73. package/codeyam-cli/src/webserver/build/server/assets/{init-CzeBGOto.js → init-Dkas-RUS.js} +1 -1
  74. package/codeyam-cli/src/webserver/build/server/assets/server-build-pulXLTrG.js +640 -0
  75. package/codeyam-cli/src/webserver/build/server/index.js +1 -1
  76. package/codeyam-cli/src/webserver/build-info.json +5 -5
  77. package/codeyam-cli/src/webserver/scripts/journalCapture.ts +17 -0
  78. package/codeyam-cli/src/webserver/server.js +52 -14
  79. package/codeyam-cli/src/webserver/server.js.map +1 -1
  80. package/codeyam-cli/src/webserver/terminalServer.js +61 -13
  81. package/codeyam-cli/src/webserver/terminalServer.js.map +1 -1
  82. package/codeyam-cli/templates/editor-step-hook.py +21 -0
  83. package/codeyam-cli/templates/skills/codeyam-editor/SKILL.md +19 -1
  84. package/package.json +1 -1
  85. package/packages/ai/src/lib/dataStructure/ScopeDataStructure.js +4 -1
  86. package/packages/ai/src/lib/dataStructure/ScopeDataStructure.js.map +1 -1
  87. package/packages/analyze/src/lib/files/analyze/findOrCreateEntity.js +3 -2
  88. package/packages/analyze/src/lib/files/analyze/findOrCreateEntity.js.map +1 -1
  89. package/packages/analyze/src/lib/files/analyze/gatherEntityMap.js +9 -7
  90. package/packages/analyze/src/lib/files/analyze/gatherEntityMap.js.map +1 -1
  91. package/packages/analyze/src/lib/files/analyzeChange.js +1 -0
  92. package/packages/analyze/src/lib/files/analyzeChange.js.map +1 -1
  93. package/packages/analyze/src/lib/files/analyzeInitial.js +1 -0
  94. package/packages/analyze/src/lib/files/analyzeInitial.js.map +1 -1
  95. package/packages/database/src/lib/loadAnalysis.js +1 -1
  96. package/packages/database/src/lib/loadAnalysis.js.map +1 -1
  97. package/packages/database/src/lib/loadEntity.js +5 -5
  98. package/packages/database/src/lib/loadEntity.js.map +1 -1
  99. package/packages/utils/src/lib/fs/rsyncCopy.js +22 -1
  100. package/packages/utils/src/lib/fs/rsyncCopy.js.map +1 -1
  101. package/codeyam-cli/src/webserver/build/client/assets/cy-logo-cli-DcX-ZS3p.js +0 -1
  102. package/codeyam-cli/src/webserver/build/client/assets/editor.entity.(_sha)-CGzKlIHg.js +0 -58
  103. package/codeyam-cli/src/webserver/build/client/assets/editorPreview-oepecPae.js +0 -41
  104. package/codeyam-cli/src/webserver/build/client/assets/globals-Yn9W3zp3.css +0 -1
  105. package/codeyam-cli/src/webserver/build/client/assets/manifest-2ef99f38.js +0 -1
  106. package/codeyam-cli/src/webserver/build/server/assets/analysisRunner-BPmOG9bE.js +0 -13
  107. package/codeyam-cli/src/webserver/build/server/assets/server-build-Dht7CKXY.js +0 -552
@@ -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, isOnlyIncompleteEntities, isAutoRemediable, identifyScenariosNeedingRecapture, detectDuplicateNames, } from "../editorAudit.js";
3
+ import { isComponent, classifyGlossaryEntries, computeAudit, filterGlossaryByChangeStatus, resolveAuditSessionScope, queryScenarioCounts, queryPageScenarioCounts, queryIncompleteEntities, queryMiscategorizedScenarios, queryUnassociatedScenarios, isOnlyIncompleteEntities, isOnlyPreExistingIncomplete, isAutoRemediable, identifyScenariosNeedingRecapture, detectDuplicateNames, } 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;
@@ -1717,7 +1990,10 @@ describe('editorAudit', () => {
1717
1990
  });
1718
1991
  // ── isAutoRemediable ─────────────────────────────────────────────────
1719
1992
  describe('isAutoRemediable', () => {
1720
- it('should return true on first attempt when only incomplete entities', () => {
1993
+ // isAutoRemediable always returns false the audit never triggers
1994
+ // full analyze-imports inline. It takes minutes on large projects.
1995
+ // Only lightweight backfill is acceptable during audit.
1996
+ it('should return false even on first attempt with only incomplete entities', () => {
1721
1997
  const result = isAutoRemediable({
1722
1998
  componentsMissing: 0,
1723
1999
  componentsWithErrors: 0,
@@ -1729,11 +2005,9 @@ describe('editorAudit', () => {
1729
2005
  incompleteEntities: 3,
1730
2006
  allPassing: false,
1731
2007
  }, false);
1732
- expect(result).toBe(true);
2008
+ expect(result).toBe(false);
1733
2009
  });
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
2010
+ it('should return false on second attempt', () => {
1737
2011
  const result = isAutoRemediable({
1738
2012
  componentsMissing: 0,
1739
2013
  componentsWithErrors: 0,
@@ -2187,8 +2461,11 @@ describe('editorAudit', () => {
2187
2461
  },
2188
2462
  ]);
2189
2463
  });
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
2464
+ it('should skip phantom SHAs (entity_sha with no entity record)', async () => {
2465
+ // Scenario has entity_sha but entity record doesn't exist.
2466
+ // These are "phantom SHAs" created when scenarios were registered
2467
+ // without component_path — they can never be fixed by analyze-imports
2468
+ // and should not block audit progression.
2192
2469
  await db
2193
2470
  .insertInto('editor_scenarios')
2194
2471
  .values({
@@ -2201,14 +2478,8 @@ describe('editorAudit', () => {
2201
2478
  })
2202
2479
  .execute();
2203
2480
  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
- ]);
2481
+ // Phantom SHAs are excluded — not reportable as incomplete
2482
+ expect(result).toEqual([]);
2212
2483
  });
2213
2484
  it('should detect incomplete entity whose scenario predates the session', async () => {
2214
2485
  // Entity with no analyses, scenario created BEFORE session
@@ -2676,5 +2947,613 @@ describe('editorAudit', () => {
2676
2947
  expect(result.summary.componentsMissing).toBe(1);
2677
2948
  });
2678
2949
  });
2950
+ // ── queryUnassociatedScenarios ──────────────────────────────────────
2951
+ describe('queryUnassociatedScenarios', () => {
2952
+ let db;
2953
+ let rawDb;
2954
+ const projectId = 'test-project-id';
2955
+ beforeEach(async () => {
2956
+ rawDb = new Database(':memory:');
2957
+ db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
2958
+ await db.schema
2959
+ .createTable('editor_scenarios')
2960
+ .addColumn('id', 'varchar', (col) => col.primaryKey())
2961
+ .addColumn('project_id', 'varchar', (col) => col.notNull())
2962
+ .addColumn('name', 'varchar', (col) => col.notNull())
2963
+ .addColumn('component_name', 'varchar')
2964
+ .addColumn('component_path', 'varchar')
2965
+ .addColumn('entity_sha', 'varchar')
2966
+ .addColumn('display_name', 'varchar')
2967
+ .addColumn('page_file_path', 'varchar')
2968
+ .addColumn('url', 'varchar')
2969
+ .addColumn('type', 'varchar')
2970
+ .addColumn('created_at', 'datetime')
2971
+ .addColumn('updated_at', 'datetime')
2972
+ .execute();
2973
+ });
2974
+ afterEach(async () => {
2975
+ await db.destroy();
2976
+ });
2977
+ it('should return empty when all scenarios have entity_sha', async () => {
2978
+ await db
2979
+ .insertInto('editor_scenarios')
2980
+ .values({
2981
+ id: 'sc-1',
2982
+ project_id: projectId,
2983
+ name: 'Header - Default',
2984
+ component_name: 'Header',
2985
+ component_path: 'src/components/Header.tsx',
2986
+ entity_sha: 'sha-header',
2987
+ created_at: '2026-03-16 23:00:00',
2988
+ updated_at: '2026-03-16 23:00:00',
2989
+ })
2990
+ .execute();
2991
+ const result = await queryUnassociatedScenarios(db, projectId, null);
2992
+ expect(result).toEqual([]);
2993
+ });
2994
+ it('should find component scenarios with NULL entity_sha', async () => {
2995
+ // This reproduces the Margo testapp bug: subagent registered scenarios
2996
+ // but entity records didn't exist yet, so entity_sha was never set
2997
+ await db
2998
+ .insertInto('editor_scenarios')
2999
+ .values({
3000
+ id: 'sc-1',
3001
+ project_id: projectId,
3002
+ name: 'SearchBar - Default',
3003
+ component_name: 'SearchBar',
3004
+ component_path: 'src/components/SearchBar.tsx',
3005
+ entity_sha: null,
3006
+ created_at: '2026-03-20 18:45:00',
3007
+ updated_at: '2026-03-20 18:45:00',
3008
+ })
3009
+ .execute();
3010
+ await db
3011
+ .insertInto('editor_scenarios')
3012
+ .values({
3013
+ id: 'sc-2',
3014
+ project_id: projectId,
3015
+ name: 'SearchBar - With Results',
3016
+ component_name: 'SearchBar',
3017
+ component_path: 'src/components/SearchBar.tsx',
3018
+ entity_sha: null,
3019
+ created_at: '2026-03-20 18:45:05',
3020
+ updated_at: '2026-03-20 18:45:05',
3021
+ })
3022
+ .execute();
3023
+ const result = await queryUnassociatedScenarios(db, projectId, null);
3024
+ expect(result).toHaveLength(1);
3025
+ expect(result[0].name).toBe('SearchBar');
3026
+ expect(result[0].filePath).toBe('src/components/SearchBar.tsx');
3027
+ expect(result[0].scenarioCount).toBe(2);
3028
+ expect(result[0].scenarioNames).toEqual(expect.arrayContaining([
3029
+ 'SearchBar - Default',
3030
+ 'SearchBar - With Results',
3031
+ ]));
3032
+ });
3033
+ it('should find page scenarios with NULL entity_sha', async () => {
3034
+ await db
3035
+ .insertInto('editor_scenarios')
3036
+ .values({
3037
+ id: 'sc-1',
3038
+ project_id: projectId,
3039
+ name: 'Full Page — Rich Library',
3040
+ component_name: null,
3041
+ component_path: null,
3042
+ page_file_path: 'src/library/LibraryApp.tsx',
3043
+ entity_sha: null,
3044
+ created_at: '2026-03-20 18:50:00',
3045
+ updated_at: '2026-03-20 18:50:00',
3046
+ })
3047
+ .execute();
3048
+ const result = await queryUnassociatedScenarios(db, projectId, null);
3049
+ expect(result).toHaveLength(1);
3050
+ expect(result[0].name).toBe('LibraryApp');
3051
+ expect(result[0].filePath).toBe('src/library/LibraryApp.tsx');
3052
+ expect(result[0].scenarioCount).toBe(1);
3053
+ });
3054
+ it('should ignore scenarios without any file path (orphans without component_path or page_file_path)', async () => {
3055
+ // Scenarios with no file path at all can't be associated — they're not
3056
+ // actionable, so don't report them as unassociated
3057
+ await db
3058
+ .insertInto('editor_scenarios')
3059
+ .values({
3060
+ id: 'sc-1',
3061
+ project_id: projectId,
3062
+ name: 'Some Orphan',
3063
+ component_name: null,
3064
+ component_path: null,
3065
+ page_file_path: null,
3066
+ entity_sha: null,
3067
+ created_at: '2026-03-20 18:50:00',
3068
+ updated_at: '2026-03-20 18:50:00',
3069
+ })
3070
+ .execute();
3071
+ const result = await queryUnassociatedScenarios(db, projectId, null);
3072
+ expect(result).toEqual([]);
3073
+ });
3074
+ it('should group multiple components separately', async () => {
3075
+ // Two different components both missing entity_sha
3076
+ await db
3077
+ .insertInto('editor_scenarios')
3078
+ .values({
3079
+ id: 'sc-1',
3080
+ project_id: projectId,
3081
+ name: 'FullPageHeader - Default',
3082
+ component_name: 'FullPageHeader',
3083
+ component_path: 'src/components/FullPageHeader.tsx',
3084
+ entity_sha: null,
3085
+ created_at: '2026-03-20 18:45:00',
3086
+ updated_at: '2026-03-20 18:45:00',
3087
+ })
3088
+ .execute();
3089
+ await db
3090
+ .insertInto('editor_scenarios')
3091
+ .values({
3092
+ id: 'sc-2',
3093
+ project_id: projectId,
3094
+ name: 'SaveConfirmation - Visible',
3095
+ component_name: 'SaveConfirmation',
3096
+ component_path: 'src/components/SaveConfirmation.tsx',
3097
+ entity_sha: null,
3098
+ created_at: '2026-03-20 19:00:00',
3099
+ updated_at: '2026-03-20 19:00:00',
3100
+ })
3101
+ .execute();
3102
+ const result = await queryUnassociatedScenarios(db, projectId, null);
3103
+ expect(result).toHaveLength(2);
3104
+ const names = result.map((r) => r.name).sort();
3105
+ expect(names).toEqual(['FullPageHeader', 'SaveConfirmation']);
3106
+ });
3107
+ it('should only include scenarios from the specified project', async () => {
3108
+ await db
3109
+ .insertInto('editor_scenarios')
3110
+ .values({
3111
+ id: 'sc-1',
3112
+ project_id: 'other-project',
3113
+ name: 'SearchBar - Default',
3114
+ component_name: 'SearchBar',
3115
+ component_path: 'src/components/SearchBar.tsx',
3116
+ entity_sha: null,
3117
+ created_at: '2026-03-20 18:45:00',
3118
+ updated_at: '2026-03-20 18:45:00',
3119
+ })
3120
+ .execute();
3121
+ const result = await queryUnassociatedScenarios(db, projectId, null);
3122
+ expect(result).toEqual([]);
3123
+ });
3124
+ it('should scope to feature session when featureStartedAt is provided', async () => {
3125
+ // Pre-existing unassociated scenario (before session)
3126
+ await db
3127
+ .insertInto('editor_scenarios')
3128
+ .values({
3129
+ id: 'sc-old',
3130
+ project_id: projectId,
3131
+ name: 'OldComponent - Default',
3132
+ component_name: 'OldComponent',
3133
+ component_path: 'src/components/OldComponent.tsx',
3134
+ entity_sha: null,
3135
+ created_at: '2026-03-19 10:00:00',
3136
+ updated_at: '2026-03-19 10:00:00',
3137
+ })
3138
+ .execute();
3139
+ // New unassociated scenario (during session)
3140
+ await db
3141
+ .insertInto('editor_scenarios')
3142
+ .values({
3143
+ id: 'sc-new',
3144
+ project_id: projectId,
3145
+ name: 'NewComponent - Default',
3146
+ component_name: 'NewComponent',
3147
+ component_path: 'src/components/NewComponent.tsx',
3148
+ entity_sha: null,
3149
+ created_at: '2026-03-20 18:45:00',
3150
+ updated_at: '2026-03-20 18:45:00',
3151
+ })
3152
+ .execute();
3153
+ const result = await queryUnassociatedScenarios(db, projectId, '2026-03-20T18:00:00.000Z');
3154
+ // Should only find the session-scoped one
3155
+ expect(result).toHaveLength(1);
3156
+ expect(result[0].name).toBe('NewComponent');
3157
+ });
3158
+ it('should include re-registered scenarios (updated_at in session) even if created before', async () => {
3159
+ await db
3160
+ .insertInto('editor_scenarios')
3161
+ .values({
3162
+ id: 'sc-1',
3163
+ project_id: projectId,
3164
+ name: 'SearchBar - Default',
3165
+ component_name: 'SearchBar',
3166
+ component_path: 'src/components/SearchBar.tsx',
3167
+ entity_sha: null,
3168
+ created_at: '2026-03-19 10:00:00',
3169
+ updated_at: '2026-03-20 18:45:00', // re-registered during session
3170
+ })
3171
+ .execute();
3172
+ const result = await queryUnassociatedScenarios(db, projectId, '2026-03-20T18:00:00.000Z');
3173
+ expect(result).toHaveLength(1);
3174
+ expect(result[0].name).toBe('SearchBar');
3175
+ });
3176
+ it('should not include scenarios with entity_sha set (even if stale)', async () => {
3177
+ // This scenario has an entity_sha — even if it's stale, that's a
3178
+ // different problem (handled by queryIncompleteEntities)
3179
+ await db
3180
+ .insertInto('editor_scenarios')
3181
+ .values({
3182
+ id: 'sc-1',
3183
+ project_id: projectId,
3184
+ name: 'Header - Default',
3185
+ component_name: 'Header',
3186
+ component_path: 'src/components/Header.tsx',
3187
+ entity_sha: 'sha-old-version',
3188
+ created_at: '2026-03-20 18:45:00',
3189
+ updated_at: '2026-03-20 18:45:00',
3190
+ })
3191
+ .execute();
3192
+ const result = await queryUnassociatedScenarios(db, projectId, null);
3193
+ expect(result).toEqual([]);
3194
+ });
3195
+ });
3196
+ // ── isAutoRemediable with unassociatedScenarios ────────────────────
3197
+ describe('isAutoRemediable always returns false (no inline full analysis)', () => {
3198
+ // Full analyze-imports takes minutes on large projects. The audit should
3199
+ // never trigger it — only the lightweight backfill path is acceptable.
3200
+ it('should return false for unassociatedScenarios only', () => {
3201
+ expect(isAutoRemediable({ unassociatedScenarios: 3 }, false)).toBe(false);
3202
+ });
3203
+ it('should return false for incompleteEntities + unassociatedScenarios', () => {
3204
+ expect(isAutoRemediable({ incompleteEntities: 1, unassociatedScenarios: 2 }, false)).toBe(false);
3205
+ });
3206
+ it('should return false even with no other failures', () => {
3207
+ expect(isAutoRemediable({ unassociatedScenarios: 2 }, false)).toBe(false);
3208
+ });
3209
+ it('should return false when already attempted', () => {
3210
+ expect(isAutoRemediable({ unassociatedScenarios: 3 }, true)).toBe(false);
3211
+ });
3212
+ });
3213
+ describe('suggestedTestFile for functions without testFile', () => {
3214
+ it('should suggest conventional .test.ts path when testFile is undefined', () => {
3215
+ const result = computeAudit({
3216
+ components: [],
3217
+ functions: [
3218
+ { name: 'useLibraryShell', filePath: 'app/library/context.tsx' },
3219
+ ],
3220
+ scenarioCounts: {},
3221
+ testFileExistence: {},
3222
+ });
3223
+ expect(result.functions[0].suggestedTestFile).toBe('app/library/context.test.ts');
3224
+ expect(result.functions[0].status).toBe('missing');
3225
+ });
3226
+ it('should suggest .test.ts for .ts files', () => {
3227
+ const result = computeAudit({
3228
+ components: [],
3229
+ functions: [{ name: 'calculatePrice', filePath: 'app/lib/pricing.ts' }],
3230
+ scenarioCounts: {},
3231
+ testFileExistence: {},
3232
+ });
3233
+ expect(result.functions[0].suggestedTestFile).toBe('app/lib/pricing.test.ts');
3234
+ });
3235
+ it('should not set suggestedTestFile when testFile is already specified', () => {
3236
+ const result = computeAudit({
3237
+ components: [],
3238
+ functions: [
3239
+ {
3240
+ name: 'calculatePrice',
3241
+ filePath: 'app/lib/pricing.ts',
3242
+ testFile: 'app/lib/pricing.test.ts',
3243
+ },
3244
+ ],
3245
+ scenarioCounts: {},
3246
+ testFileExistence: { 'app/lib/pricing.test.ts': true },
3247
+ });
3248
+ expect(result.functions[0].suggestedTestFile).toBeUndefined();
3249
+ });
3250
+ });
3251
+ describe('hint for missing components', () => {
3252
+ it('should hint that layout files need app-level scenarios', () => {
3253
+ const result = computeAudit({
3254
+ components: [
3255
+ { name: 'LibraryLayout', filePath: 'app/library/layout.tsx' },
3256
+ ],
3257
+ functions: [],
3258
+ scenarioCounts: {},
3259
+ testFileExistence: {},
3260
+ });
3261
+ expect(result.components[0].hint).toContain('layout');
3262
+ expect(result.components[0].hint).toContain('pageFilePath');
3263
+ });
3264
+ it('should hint that page files need app-level scenarios', () => {
3265
+ const result = computeAudit({
3266
+ components: [
3267
+ { name: 'InboxPage', filePath: 'app/library/inbox/page.tsx' },
3268
+ ],
3269
+ functions: [],
3270
+ scenarioCounts: {},
3271
+ testFileExistence: {},
3272
+ });
3273
+ expect(result.components[0].hint).toContain('page');
3274
+ expect(result.components[0].hint).toContain('pageFilePath');
3275
+ });
3276
+ it('should hint that regular components need isolation routes', () => {
3277
+ const result = computeAudit({
3278
+ components: [
3279
+ { name: 'DrinkCard', filePath: 'app/components/DrinkCard.tsx' },
3280
+ ],
3281
+ functions: [],
3282
+ scenarioCounts: {},
3283
+ testFileExistence: {},
3284
+ });
3285
+ expect(result.components[0].hint).toContain('isolated-components');
3286
+ });
3287
+ it('should not set hint when component has scenarios', () => {
3288
+ const result = computeAudit({
3289
+ components: [
3290
+ { name: 'DrinkCard', filePath: 'app/components/DrinkCard.tsx' },
3291
+ ],
3292
+ functions: [],
3293
+ scenarioCounts: { DrinkCard: 2 },
3294
+ testFileExistence: {},
3295
+ });
3296
+ expect(result.components[0].hint).toBeUndefined();
3297
+ });
3298
+ });
3299
+ describe('formatIncompleteEntityGuidance', () => {
3300
+ it('should include the entity name and scenario count', () => {
3301
+ const { formatIncompleteEntityGuidance } = require('../editorAudit');
3302
+ const result = formatIncompleteEntityGuidance({
3303
+ entitySha: 'abc123',
3304
+ name: 'RuleBuilder',
3305
+ scenarioCount: 5,
3306
+ preExisting: false,
3307
+ });
3308
+ expect(result).toContain('RuleBuilder');
3309
+ expect(result).toContain('5');
3310
+ });
3311
+ it('should tell Claude the exact fix command', () => {
3312
+ const { formatIncompleteEntityGuidance } = require('../editorAudit');
3313
+ const result = formatIncompleteEntityGuidance({
3314
+ entitySha: 'abc123',
3315
+ name: 'RuleBuilder',
3316
+ scenarioCount: 5,
3317
+ preExisting: false,
3318
+ });
3319
+ expect(result).toContain('codeyam editor analyze-imports');
3320
+ });
3321
+ it('should flag pre-existing issues as non-blocking', () => {
3322
+ const { formatIncompleteEntityGuidance } = require('../editorAudit');
3323
+ const result = formatIncompleteEntityGuidance({
3324
+ entitySha: 'abc123',
3325
+ name: 'RuleBuilder',
3326
+ scenarioCount: 5,
3327
+ preExisting: true,
3328
+ });
3329
+ expect(result).toContain('pre-existing');
3330
+ });
3331
+ it('should explain what incomplete means', () => {
3332
+ const { formatIncompleteEntityGuidance } = require('../editorAudit');
3333
+ const result = formatIncompleteEntityGuidance({
3334
+ entitySha: 'abc123',
3335
+ name: 'RuleBuilder',
3336
+ scenarioCount: 5,
3337
+ preExisting: false,
3338
+ });
3339
+ // Should explain the root cause, not just the symptom
3340
+ expect(result).toMatch(/scenario.*without.*import graph|import graph.*not.*built/i);
3341
+ });
3342
+ });
3343
+ describe('getIncompleteEntityFilePaths', () => {
3344
+ // The audit should auto-fix incomplete entities by running analysis on
3345
+ // just their specific file paths, not all 117+ files. This function
3346
+ // resolves entity SHAs to file paths for targeted analysis.
3347
+ let db;
3348
+ let rawDb;
3349
+ beforeEach(async () => {
3350
+ rawDb = new Database(':memory:');
3351
+ db = new Kysely({ dialect: new SqliteDialect({ database: rawDb }) });
3352
+ await db.schema
3353
+ .createTable('entities')
3354
+ .addColumn('sha', 'varchar', (col) => col.primaryKey())
3355
+ .addColumn('name', 'varchar')
3356
+ .addColumn('file_path', 'varchar')
3357
+ .addColumn('project_id', 'varchar')
3358
+ .addColumn('entity_type', 'varchar')
3359
+ .execute();
3360
+ });
3361
+ afterEach(() => {
3362
+ rawDb.close();
3363
+ });
3364
+ it('should resolve entity SHAs to file paths from the entities table', async () => {
3365
+ const { getIncompleteEntityFilePaths } = require('../editorAudit');
3366
+ await db
3367
+ .insertInto('entities')
3368
+ .values([
3369
+ {
3370
+ sha: 'sha-rule',
3371
+ name: 'RuleBuilder',
3372
+ file_path: 'app/components/RuleBuilder.tsx',
3373
+ project_id: 'p1',
3374
+ entity_type: 'component',
3375
+ },
3376
+ {
3377
+ sha: 'sha-row',
3378
+ name: 'ArticleTableRow',
3379
+ file_path: 'app/components/ArticleTableRow.tsx',
3380
+ project_id: 'p1',
3381
+ entity_type: 'component',
3382
+ },
3383
+ ])
3384
+ .execute();
3385
+ const result = await getIncompleteEntityFilePaths(db, [
3386
+ {
3387
+ entitySha: 'sha-rule',
3388
+ name: 'RuleBuilder',
3389
+ scenarioCount: 5,
3390
+ preExisting: false,
3391
+ },
3392
+ {
3393
+ entitySha: 'sha-row',
3394
+ name: 'ArticleTableRow',
3395
+ scenarioCount: 2,
3396
+ preExisting: false,
3397
+ },
3398
+ ]);
3399
+ expect(result).toContain('app/components/RuleBuilder.tsx');
3400
+ expect(result).toContain('app/components/ArticleTableRow.tsx');
3401
+ expect(result).toHaveLength(2);
3402
+ });
3403
+ it('should skip entities whose SHA is not in the entities table', async () => {
3404
+ const { getIncompleteEntityFilePaths } = require('../editorAudit');
3405
+ const result = await getIncompleteEntityFilePaths(db, [
3406
+ {
3407
+ entitySha: 'nonexistent-sha',
3408
+ name: 'Ghost',
3409
+ scenarioCount: 1,
3410
+ preExisting: false,
3411
+ },
3412
+ ]);
3413
+ expect(result).toHaveLength(0);
3414
+ });
3415
+ it('should deduplicate file paths', async () => {
3416
+ const { getIncompleteEntityFilePaths } = require('../editorAudit');
3417
+ await db
3418
+ .insertInto('entities')
3419
+ .values([
3420
+ {
3421
+ sha: 'sha-v1',
3422
+ name: 'Foo',
3423
+ file_path: 'app/Foo.tsx',
3424
+ project_id: 'p1',
3425
+ entity_type: 'component',
3426
+ },
3427
+ {
3428
+ sha: 'sha-v2',
3429
+ name: 'Foo',
3430
+ file_path: 'app/Foo.tsx',
3431
+ project_id: 'p1',
3432
+ entity_type: 'component',
3433
+ },
3434
+ ])
3435
+ .execute();
3436
+ const result = await getIncompleteEntityFilePaths(db, [
3437
+ {
3438
+ entitySha: 'sha-v1',
3439
+ name: 'Foo',
3440
+ scenarioCount: 3,
3441
+ preExisting: false,
3442
+ },
3443
+ {
3444
+ entitySha: 'sha-v2',
3445
+ name: 'Foo',
3446
+ scenarioCount: 1,
3447
+ preExisting: false,
3448
+ },
3449
+ ]);
3450
+ expect(result).toEqual(['app/Foo.tsx']);
3451
+ });
3452
+ });
3453
+ describe('isAutoRemediable never triggers full analysis', () => {
3454
+ // The audit must NEVER run handleAnalyzeImports inline — it takes minutes
3455
+ // for large projects. Auto-remediation should only do the lightweight
3456
+ // entity SHA backfill. isAutoRemediable is now always false; the callers
3457
+ // use needsBackfillOnly for the fast path instead.
3458
+ it('should always return false regardless of summary state', () => {
3459
+ expect(isAutoRemediable({ incompleteEntities: 5 }, false)).toBe(false);
3460
+ expect(isAutoRemediable({ unassociatedScenarios: 3 }, false)).toBe(false);
3461
+ expect(isAutoRemediable({ incompleteEntities: 1, unassociatedScenarios: 2 }, false)).toBe(false);
3462
+ });
3463
+ });
3464
+ // ── isOnlyPreExistingIncomplete ─────────────────────────────────────
3465
+ describe('isOnlyPreExistingIncomplete', () => {
3466
+ it('should return true when all incomplete entities are pre-existing and no other failures', () => {
3467
+ expect(isOnlyPreExistingIncomplete({
3468
+ incompleteEntities: 2,
3469
+ preExistingIncompleteEntities: 2,
3470
+ componentsMissing: 0,
3471
+ componentsWithErrors: 0,
3472
+ functionsFailing: 0,
3473
+ functionsNameMismatch: 0,
3474
+ functionsMissing: 0,
3475
+ missingFromGlossary: 0,
3476
+ miscategorizedScenarios: 0,
3477
+ scenariosNeedingRecapture: 0,
3478
+ }, [
3479
+ {
3480
+ entitySha: 'a',
3481
+ name: 'RuleBuilder',
3482
+ scenarioCount: 5,
3483
+ preExisting: true,
3484
+ },
3485
+ {
3486
+ entitySha: 'b',
3487
+ name: 'ArticleTableRow',
3488
+ scenarioCount: 2,
3489
+ preExisting: true,
3490
+ },
3491
+ ])).toBe(true);
3492
+ });
3493
+ it('should return false when some incomplete entities are NOT pre-existing', () => {
3494
+ expect(isOnlyPreExistingIncomplete({
3495
+ incompleteEntities: 2,
3496
+ preExistingIncompleteEntities: 1,
3497
+ componentsMissing: 0,
3498
+ componentsWithErrors: 0,
3499
+ functionsFailing: 0,
3500
+ functionsNameMismatch: 0,
3501
+ functionsMissing: 0,
3502
+ missingFromGlossary: 0,
3503
+ miscategorizedScenarios: 0,
3504
+ scenariosNeedingRecapture: 0,
3505
+ }, [
3506
+ {
3507
+ entitySha: 'a',
3508
+ name: 'RuleBuilder',
3509
+ scenarioCount: 5,
3510
+ preExisting: true,
3511
+ },
3512
+ {
3513
+ entitySha: 'b',
3514
+ name: 'NewComponent',
3515
+ scenarioCount: 1,
3516
+ preExisting: false,
3517
+ },
3518
+ ])).toBe(false);
3519
+ });
3520
+ it('should return false when there are other failures besides incomplete entities', () => {
3521
+ expect(isOnlyPreExistingIncomplete({
3522
+ incompleteEntities: 2,
3523
+ preExistingIncompleteEntities: 2,
3524
+ componentsMissing: 1,
3525
+ }, [
3526
+ {
3527
+ entitySha: 'a',
3528
+ name: 'RuleBuilder',
3529
+ scenarioCount: 5,
3530
+ preExisting: true,
3531
+ },
3532
+ {
3533
+ entitySha: 'b',
3534
+ name: 'ArticleTableRow',
3535
+ scenarioCount: 2,
3536
+ preExisting: true,
3537
+ },
3538
+ ])).toBe(false);
3539
+ });
3540
+ it('should return false when incomplete entities array is empty', () => {
3541
+ expect(isOnlyPreExistingIncomplete({ incompleteEntities: 0 }, [])).toBe(false);
3542
+ });
3543
+ it('should return false when incomplete entities array is missing', () => {
3544
+ expect(isOnlyPreExistingIncomplete({ incompleteEntities: 2, preExistingIncompleteEntities: 2 }, undefined)).toBe(false);
3545
+ });
3546
+ });
3547
+ describe('isOnlyIncompleteEntities with unassociatedScenarios', () => {
3548
+ it('should return true when only unassociatedScenarios present', () => {
3549
+ expect(isOnlyIncompleteEntities({ unassociatedScenarios: 5 })).toBe(true);
3550
+ });
3551
+ it('should return false when unassociatedScenarios present with other failures', () => {
3552
+ expect(isOnlyIncompleteEntities({
3553
+ unassociatedScenarios: 5,
3554
+ functionsMissing: 1,
3555
+ })).toBe(false);
3556
+ });
3557
+ });
2679
3558
  });
2680
3559
  //# sourceMappingURL=editorAudit.test.js.map