Add "Sync" command - #1205
Open
isc-dchui wants to merge 39 commits into
Open
Conversation
isc-dchui
requested review from
isc-cborbonm,
isc-egabhart,
isc-eneil,
isc-jili,
isc-jlechtne,
isc-kiyer,
isc-pbarton and
isc-tleavitt
as code owners
July 22, 2026 19:16
…flesh out verbose mode output
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Motivation
The primary use case is LLM-assisted development: an LLM generates or edits
.cls/.inc/.macfiles on disk, then the developer runssyncto reload and recompile only what changed without a fullreloadthat reinstalls the entire module. This makes the tight edit-compile-test loop fast enough to be practical inside an agentic coding session.Secondary use case: any developer editing IPM-managed source who wants faster incremental feedback than
reloadprovides.Architecture
Entry point
Syncis a first-class lifecycle phase registered in%IPM.DataType.PhaseName. It fires the standardOnBeforePhase/OnAfterPhase/<Invoke>hooks for free. With no module name,Main.Syncloops over allDeveloperMode=1modules and collects failures without throwing.Flags:
-delete(remove deleted files from server),-test/-t(run changed test cases),-verbose/-v.10-step pipeline (
%IPM.General.Sync.Pipeline.Run)%IPM.Lifecycle.Base:%Syncis a thin instance-method delegator (must be an instance method soExecutePhases'$methoddispatch finds it); it just calls##class(%IPM.General.Sync.Pipeline).Run(..Module, .params). The orchestration and itsSync*helpers live on%IPM.General.Sync.Pipeline.SyncCheckModuleXmlmodule.xml; if changed, reload manifest immediatelyFileHash.CollectScanDirsGetSyncDirectory()on eachSupportsSync()=1processor;DeduplicateScanDirs; thenWalkAndHashDirstargeted walk intoallFiles/allHashesSyncBuildReverseIndexrelPath → processorvia (a)ResolveChildren+OnItemRelativePathfor individually-declared files, (b)GetSyncDirectory()prefix scan for directory-owned resourcesFileHash.ComputeChangesSyncRoutePathSetsyncByResourceSyncDispatchProcessorsOnSyncon each routed processorSyncCompileSyncApplyDeletes-delete: delete server-side docs, then recompileFileHash.CommitChangesSyncRunTests-test: run changed test-phase cases via owningTestprocessor onlyTargeted-walk architecture (
GetSyncDirectory)Each processor returns the single directory it owns as a normalized relative path (no leading slash; a trailing slash is fine). Sync walks only those directories, never the full module root. This avoids touching
build/,data/,node_modules/, etc. entirely.DeduplicateScanDirsremoves any entry that is a subdirectory of an already-kept entry (alphabetical scan, prefix check) to prevent double-walking.Abstract.GetSyncDirectory()returns""(sentinel: no owned directory). All callers guardsyncDir '= "".Change detection (
FileHash.ComputeChanges)Signature:
ComputeChanges(module, .modified, .deleted, .reverseIndex, .allFiles, .allHashes). Three-pass pipeline — SHA-1 hash only, no mtime or size:allFilesdirectly;RelPathToDocNamereturns""for extensions other thancls/inc/mac/int, so only those are considered here. Checks$$$comClassDefined/%RoutineMgr.Exists— only files compiled in this namespace count.xml/rtnfiles. No I/O since paths already resolved.FileHashrow whose file is missing from disk → deleted.module.xmlis skipped explicitly — Step 1 owns it, so it never routes throughComputeChanges.No baseline row → new file → modified. Baseline exists → hash-only comparison.
Filesystem walking
WalkAndHashDirsdispatches to Pythonos.walk()(WalkAndHashFilesPython) with automatic SQL BFS fallback (WalkAndHashFilesSQL). Walk-once: step 2 walks all declared dirs intoallFiles+allHashes, shared by bothSyncBuildReverseIndexandComputeChanges.Stage rollout
AbstractCompilablederivatives,Test(test-phase only)FileCopy,WebApplication,PythonWheelCPF,Copy,ArtifactoryTarball,LegacyLocalizedMessages,Default.GlobalCSPApplication,SystemSetting,ModuleExport,LocalizationExportPeculiar implementation details
%-prefixed classes aren't caught by Pass 1.RelPathToDocName("src/cls/IPM/Main.cls")→"IPM.Main.CLS", but the real class is%IPM.Main; nothing recovers the%, so$$$comClassDefined("IPM.Main")is false and Pass 1 skips it. They're caught in Pass 2 instead, via their reverse-index entry (step 3a).Test files are tracked by on-disk path, not by resolving to a server class. A test class at
tests/unit/SyncTest/Tests/Trivial.clsmaps to"tests.unit.SyncTest.Tests.Trivial.CLS"— a plausible name that isn't the real class, so$$$comClassDefinedis false and Pass 1's namespace check would wrongly skip it. Two mechanisms compensate: test files reach the reverse index via theGetSyncDirectory()prefix scan (step 3b) and are detected in Pass 2 rather than Pass 1, andStampModulestamps every compilable file in a scan dir unconditionally, never consulting the namespace.One helper owns scan-dir collection so stamping and syncing can't disagree. The "collect each processor's
GetSyncDirectory()+DeduplicateScanDirs" loop is the singleFileHash.CollectScanDirs, called by bothStampModule(baseline stamping) andPipeline.Run(step 2) — one source of truth for which dirs are in scope.Deleting a test file needs
Test.OnSyncto delete the server doc itself.SyncApplyDeleteshandles onlyAbstractCompilableresources, skippingTest, soOnSyncmust call$system.OBJ.Deletefor removed test class files directly. Otherwise the server-side%UnitTest.TestCasesubclass persists with no on-disk source and no hash record — permanently invisible to future syncs.A failed compile leaves the baseline untouched so the next sync retries. If compile fails,
CommitChangesis skipped; changed files keep their old hash rows (or none, if new), so the next sync re-detects and retries them once the error is fixed.A pre-feature module self-heals its baseline on first sync. A module installed before this feature has zero
FileHashrows. The first sync stamps a baseline from current disk state and exits; only the next sync after a real edit detects changes.Performance: what was tried and why things changed
os.walk()because IRIS's%Library.File_FileSetSQL walk has severe per-row overhead on large directories. Python processes the entire tree in native C and returns results in bulk.zsearch/$zsearchGetSyncDirectory()architecture: avoid enteringbuild/,node_modules/,data/entirely rather than filtering them after the I/O.c37ac2d) to avoid double-traversal overhead.080e6bed): too complex, too slow. Only canonical resource paths (declaredName/Directoryattributes) are supported.Testing
All integration tests in
Test.PM.Integration.Sync. Fixture:tests/integration_tests/Test/PM/Integration/_data/sync-test/is copied once to a native-filesystem "pristine" dir inOnBeforeAllTests(the bind-mounted_data/is slow for the many small-file copies), then restored from there and loaded in dev mode before each test, uninstalled and deleted after.TestSyncAllProcessesDependenciesFirstuses a separate three-module fixture set (_data/sync-dep/{a,b,c}/).TestGetStoredPathsReturnsStampedPathsStampModulerecords expected paths inFileHashafterloadTestNoChangeIsNoOpTestMigrationFromNoBaselineTestModifiedClassRecompiles.clsfile detected and recompiledTestModifiedXmlClassRecompilesFormat="XML"class file syncs identically to UDL; reloaded class reflects edited valueTestSuperclassEditRecompilesSubclassTestIncludeEditRecompilesConsumer.inccauses consumer class to recompileTestUntrackedFileIgnoredTestDeleteSkippedByDefault-deleteflagTestDeleteTestClassRemovesFromServer-deleteTestDeleteRecompilesDependentsTestModuleXmlChangedWarningmodule.xmlreloads manifest and emits warningTestSyncTestFlag-testruns changed test class; without flag, loads but doesn't runTestSyncTestFlagBatchesMultipleChangedClassesTestSyncTestFlagOnlyRunsOwningResourceUnitTestprocessorsTestSyncAllDevModeModules[sync-test]specifically reports nothing to syncTestSyncAllProcessesDependenciesFirstTestSyncModuleNotFoundTestSyncNonDevModeModuleTestModuleXmlAddsResourcePicksUpNewFilemodule.xmlchange adding a new<Resource>causes that resource's files to be tracked in the same sync callTestFailedCompileRetriesTestModuleXmlAndClassEditedTogethermodule.xmland a source class in one operation syncs both changesExample
Changes made across four different modules, including modifying a test to make it fail and deleting a file. Note that the summary section appears when a namespace-sync (sync without a specified module) runs.
Checklist
mainbranch rebased or merged.zpm test -only) and integration tests (zpm verify -only) pass.