Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
"""Import explicitly declared file tasks with conservative update ownership."""
"""Plan imports of entity definitions, task references, and executable task files.

Follow logseq-entity, entity-tasks, and task-dependencies links from the pages
already selected for sync. Read each task reference's task-files mapping to
locate the entrypoint and helpers, and add their copies to the page-sync plan.
Before replacing imported task files, check the destination manifest's source
ownership, last-imported content hashes, and file modes. Update the manifest
in the same plan so later syncs can distinguish upstream changes from local
edits. core.apply_plan writes the completed plan as one recoverable update.
"""
from __future__ import annotations

import hashlib
Expand Down Expand Up @@ -64,6 +73,14 @@ def digest(data):


def compatible_config(root):
"""Check that the destination Mise config can discover imported file tasks.

Read mise.toml or .mise.toml at the graph root. When task_config.includes
is set, require mise-tasks (with an optional ./ prefix or trailing slash).
An absent includes setting uses Mise's default file-task discovery.
Reject an explicit list that excludes mise-tasks before planning writes;
malformed TOML also stops the import.
"""
for filename in ('mise.toml', '.mise.toml'):
config = root / filename
if config.exists():
Expand Down
8 changes: 4 additions & 4 deletions mise-tasks/logseq/entity/proxy/page/sync
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,13 @@ def main():
plan = build_page_plan(options.source, options.destination, options.page, options.codeforge_url,
options.follow_embeds)
try:
import companions
import task_imports
except ModuleNotFoundError as error:
if error.name != 'companions':
if error.name != 'task_imports':
raise
raise SyncError('Missing companion importer lib/companions.py; restore the task implementation before syncing') from error
raise SyncError('Missing task importer lib/task_imports.py; restore the task implementation before syncing') from error
else:
companions.extend_plan(plan)
task_imports.extend_plan(plan)
print('\n'.join(plan.details))
for warning in plan.warnings:
print('Warning: ' + warning)
Expand Down
74 changes: 70 additions & 4 deletions mise-tasks/logseq/entity/proxy/page/tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,15 +277,15 @@ def test_codeforge_inference_nested_graph_encoded_filename(self):
self.assertIn('/blob/main/sub/source/pages/Book___A%253F%20B.md', text)

def test_manifest_records_repository_relative_graph(self):
import companions
import task_imports
repo = self.root / 'checkout'
repo.mkdir()
nested = self.graph('checkout/sub/source')
(nested / 'pages/Example.md').write_text('- Example\n')
subprocess.run(['git', 'init', '-q', str(repo)], check=True)
subprocess.run(['git', '-C', str(repo), 'remote', 'add', 'origin', 'git@github.com:owner/repo.git'], check=True)
plan = build_page_plan(nested, self.destination, 'Example')
companions.extend_plan(plan)
task_imports.extend_plan(plan)
manifest = plan.writes['.logseq-proxy/manifest.json'].decode()
self.assertNotIn(str(self.root), manifest)
record = json.loads(manifest)['imports']['github.com/owner/repo::sub/source::Example']
Expand Down Expand Up @@ -321,9 +321,9 @@ def commit(self, repo):
self.git(repo, 'commit', '-qm', 'fixture')

def sync(self, source, destination):
import companions
import task_imports
plan = build_page_plan(source, destination, 'Example')
companions.extend_plan(plan)
task_imports.extend_plan(plan)
apply_plan(plan)
return plan

Expand Down Expand Up @@ -398,5 +398,71 @@ def test_detached_source_worktree_url_names_commit(self):
self.assertIn('/blob/' + self.git(repo, 'rev-parse', 'HEAD') + '/', props['logseq-proxy-codeforge-url'])


class TaskImporterTests(unittest.TestCase):
setUp = PageSyncTests.setUp
tearDown = PageSyncTests.tearDown
graph = PageSyncTests.graph
plan = PageSyncTests.plan

def test_imported_sync_runs_after_helper_rename_and_retains_cleanup_candidate(self):
import task_imports
import shutil
source_files = Path('mise-tasks/logseq/entity/proxy/page')
task_dir = self.source / source_files
(task_dir / 'lib').mkdir(parents=True)
mapping = {str(source_files / 'sync'): str(source_files / 'sync'),
str(source_files / 'lib/core.py'): str(source_files / 'lib/core.py'),
str(source_files / 'lib/companions.py'): str(source_files / 'lib/companions.py')}
shutil.copyfile(LIB / 'core.py', task_dir / 'lib/core.py')
shutil.copyfile(LIB / 'task_imports.py', task_dir / 'lib/companions.py')
entrypoint = (LIB.parent / 'sync').read_text()
(task_dir / 'sync').write_text(entrypoint.replace('task_imports', 'companions'))
(task_dir / 'sync').chmod(0o755)
self.source_page.write_text('logseq-entity:: [[Example/Definition]]\n- body\n')
(self.source / 'pages/Example___Definition.md').write_text('entity-tasks:: [[Example/mise/Task/sync]]\n- Definition\n')
reference = self.source / 'pages/Example___mise___Task___sync.md'
declaration = ('task-owner:: [[Owner]]\ntask-config-root:: .\n'
'task-name:: logseq:entity:proxy:page:sync\n'
f'task-entrypoint:: {source_files}/sync\n'
f'task-files:: {json.dumps(mapping)}\n'
'source-link:: https://example.test/owner/repo/blob/main/sync\n- Sync\n')
reference.write_text(declaration)
first = self.plan()
task_imports.extend_plan(first)
apply_plan(first)
(task_dir / 'lib/companions.py').rename(task_dir / 'lib/task_imports.py')
(task_dir / 'sync').write_text(entrypoint)
reference.write_text(declaration.replace('companions.py', 'task_imports.py'))
refreshed = self.plan()
task_imports.extend_plan(refreshed)
self.assertTrue(any('companions.py: removed upstream; cleanup candidate' in item for item in refreshed.warnings))
apply_plan(refreshed)
self.assertTrue((self.destination / source_files / 'lib/companions.py').is_file())
self.assertTrue((self.destination / source_files / 'lib/task_imports.py').is_file())
result = subprocess.run([sys.executable, str(self.destination / source_files / 'sync'),
'--source', str(self.source), '--destination', str(self.destination),
'--page', self.page], text=True, capture_output=True)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn('unchanged: ' + str(source_files / 'lib/task_imports.py'), result.stdout)
repeat = self.plan()
task_imports.extend_plan(repeat)
self.assertTrue(all(data == repeat.expected[path] for path, data in repeat.writes.items()))

def test_destination_task_discovery_config_matches_documented_variants(self):
import task_imports
config = self.destination / 'mise.toml'
for text in ('', '[task_config]\nincludes = ["mise-tasks"]\n',
'[task_config]\nincludes = ["./mise-tasks/"]\n'):
with self.subTest(text=text):
config.write_text(text)
task_imports.compatible_config(self.destination)
config.write_text('[task_config]\nincludes = ["elsewhere"]\n')
with self.assertRaisesRegex(ValueError, 'must include mise-tasks'):
task_imports.compatible_config(self.destination)
config.write_text('malformed = [')
with self.assertRaises(ValueError):
task_imports.compatible_config(self.destination)


if __name__ == '__main__':
unittest.main()
8 changes: 6 additions & 2 deletions pages/Logseq___Entity___Proxy___Page.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,15 @@ entity-proxy-destination-properties:: public
- ## Executable sync
- [[Logseq/Entity/Proxy/Page/mise/Task/sync]] previews the page, entity definitions, companion task references, implementation files, and assets imported by a sync. `--apply` applies the previewed changes.
- An instance's `logseq-entity::` links lead to its source entity definitions. Each definition's `entity-tasks::` declares companion task references, whose file mappings and task dependencies follow [[Logseq/Entity/Mise/Task]]. The sync visits shared and cyclic dependencies once.
- `.logseq-proxy/manifest.json` in the destination graph records source repository and graph root, logical page identity, source-to-destination file mappings, ownership, and last imported content hashes.
- The manifest retains the latest source companion declarations. When a proxy becomes the source for another garden, task discovery uses those declarations and resolves imported implementation paths through the source manifest.
- Re-sync refreshes source changes where imported implementation files still match their recorded hashes. Local implementation edits, unowned destination files, and competing source claims produce conflicts before application.
- Page properties and bodies refresh from the source per **Invariants** above. Files removed from upstream task mappings are reported as cleanup candidates and retained locally.
- A preview reports logical entity definitions without files separately from missing declared tasks or implementation files. Missing source assets remain warnings; missing task requirements stop application.
- ## Import manifest
- `.logseq-proxy/manifest.json` in the destination graph records source repository and graph path, logical page identity, source-to-destination file mappings, ownership, and last imported content hashes and modes. It accompanies the imported pages and task files in version control.
- `files` records implementation ownership and the last import's hash and mode; `pages` records source identity for each logical page. `tasks` retains each task's identity and file mapping; `imports` records the implementation paths associated with each requested page.
- `declarations` retains the latest source entity and task properties. When a proxy becomes the source for another garden, task discovery uses those saved declarations and resolves imported implementation paths through the source manifest.
- A re-sync compares each imported implementation file with its recorded hash and mode before replacing it. Deleting the manifest loses that baseline; existing task files then appear unowned and require reconciliation before importing again.
- [The illustrated manifest explanation](../mise-tasks/logseq/entity/proxy/page/docs/index.html#manifest) describes the sections, declaration precedence, and update checks. [[Logseq/Entity/Mise/Task]] specifies the `source-link::` and `task-files::` declarations from which task imports are planned.
- ## Examples in this garden
- [[Book/ML with PyTorch and Scikit-Learn]]
- [[Person/Thomas Parr/Book/2022/Active Inference]] (legacy name shape)
4 changes: 2 additions & 2 deletions pages/Logseq___Entity___Proxy___Page___mise___Task___sync.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ task-owner:: [[Person/codekiln/GitHub/logseq-encode-garden]]
task-config-root:: .
task-name:: logseq:entity:proxy:page:sync
task-entrypoint:: mise-tasks/logseq/entity/proxy/page/sync
task-files:: {"mise-tasks/logseq/entity/proxy/page/sync":"mise-tasks/logseq/entity/proxy/page/sync","mise-tasks/logseq/entity/proxy/page/lib/core.py":"mise-tasks/logseq/entity/proxy/page/lib/core.py","mise-tasks/logseq/entity/proxy/page/lib/companions.py":"mise-tasks/logseq/entity/proxy/page/lib/companions.py"}
task-files:: {"mise-tasks/logseq/entity/proxy/page/sync":"mise-tasks/logseq/entity/proxy/page/sync","mise-tasks/logseq/entity/proxy/page/lib/core.py":"mise-tasks/logseq/entity/proxy/page/lib/core.py","mise-tasks/logseq/entity/proxy/page/lib/task_imports.py":"mise-tasks/logseq/entity/proxy/page/lib/task_imports.py"}
task-dependencies:: [[Logseq/Entity/Proxy/Page/mise/Task/docs/serve]]
source-link:: https://github.com/codekiln/logseq-encode-garden/blob/main/mise-tasks/logseq/entity/proxy/page/sync
see-also:: [[Logseq/Entity/Proxy/Page]], [[Logseq/Entity/Definition]]
Expand Down Expand Up @@ -63,7 +63,7 @@ see-also:: [[Logseq/Entity/Proxy/Page]], [[Logseq/Entity/Definition]]
~~~
- The server binds to `127.0.0.1`; `--port` selects a port, `--no-open` keeps browser selection manual, and Ctrl-C stops the server.
- ## Source and help
- [Sync file task](https://github.com/codekiln/logseq-encode-garden/blob/main/mise-tasks/logseq/entity/proxy/page/sync), [page sync implementation](https://github.com/codekiln/logseq-encode-garden/blob/main/mise-tasks/logseq/entity/proxy/page/lib/core.py), and [companion import implementation](https://github.com/codekiln/logseq-encode-garden/blob/main/mise-tasks/logseq/entity/proxy/page/lib/companions.py).
- [Sync file task](https://github.com/codekiln/logseq-encode-garden/blob/main/mise-tasks/logseq/entity/proxy/page/sync), [page sync implementation](https://github.com/codekiln/logseq-encode-garden/blob/main/mise-tasks/logseq/entity/proxy/page/lib/core.py), and [task import and manifest implementation](https://github.com/codekiln/logseq-encode-garden/blob/main/mise-tasks/logseq/entity/proxy/page/lib/task_imports.py).
- ~~~sh
mise run logseq:entity:proxy:page:sync --help
~~~