← Commit history

Add post-command document save and scratch cleanup hints without automatic mutations

John Lauer ·424b9b95bd ·29d ago ·parent 33c4ac5
4 files changed +117−1
addin/AdomBridge/commands/__init__.py+5−1
@@ -154,4 +154,8 @@ def dispatch_command(app: adsk.core.Application, command: str, args: dict) -> di             "error": f"Unknown add-in command: {command}",             "_hint": f"Valid add-in commands: {', '.join(sorted(COMMAND_HANDLERS.keys()))}.",         }-    return handler(app, args)+    result = handler(app, args)+    if command != "activity_banner":+        from .document_hygiene import attach_document_hygiene+        result = attach_document_hygiene(app, result)+    return result
addin/AdomBridge/commands/document_hygiene.pyadded+60
@@ -0,0 +1,60 @@+"""Read-only save reminders. Called on Fusion's main thread after command completion.++Exports are checkpoints, not proof that Fusion cleared its modified/recovery state.+This module never saves, closes, infers ownership from names, or deletes recovery data.+"""+++def _property(obj, name):+    try:+        return getattr(obj, name)+    except Exception:+        return None+++def inspect_documents(app):+    documents = []+    try:+        for i in range(app.documents.count):+            doc = app.documents.item(i)+            saved = _property(doc, 'isSaved')+            modified = _property(doc, 'isModified')+            documents.append({+                'name': _property(doc, 'name'),+                'isSaved': saved, 'isModified': modified,+                'needsSave': modified is True or (saved is False and modified is not False),+                'saveStateKnown': saved is not None and modified is not None,+            })+    except Exception as exc:+        return {'inspectionComplete': False, 'documents': documents, 'error': str(exc)}+    pending = [d for d in documents if d['needsSave']]+    return {'inspectionComplete': True, 'openDocumentCount': len(documents),+            'needsSaveCount': len(pending), 'documents': documents,+            'recoveryHealth': 'notInspected', 'ownership': 'notInferred'}+++def attach_document_hygiene(app, result):+    if not isinstance(result, dict):+        return result+    state = inspect_documents(app)+    result['documentHygiene'] = state+    count = state.get('needsSaveCount', 0)+    if count or state.get('openDocumentCount', 0) > 3:+        names = [d['name'] for d in state['documents'] if d['needsSave']]+        hint = (+            'Document checkpoint: %s open, %s need saving%s. '+            'Save edited work at milestones and before ending the task. A source export '+            'or Document.CopyToDesktop is a separate checkpoint and may leave Fusion '+            'isSaved=false/isModified=true; verify the artifact and re-read save state. '+            'Close only this task\'s completed scratch documents by exact identity with '+            'fusion_close_document after verifying a current native checkpoint; never '+            'bulk-discard other work. If Fusion reports recovery-file failure, checkpoint '+            'immediately, then save or close that document; do not just dismiss recovery. '+            'Use a task subfolder when saving to cloud.'+        ) % (state.get('openDocumentCount', '?'), count,+             (' (' + ', '.join(str(n) for n in names[:6]) + (', ...' if len(names)>6 else '') + ')') if names else '')+        result['_hintDocumentHygiene'] = hint+        result['_hint'] = (result.get('_hint', '') + ' ' + hint).strip()+    elif not state['inspectionComplete']:+        result['_hintDocumentHygiene'] = 'Save-state inspection incomplete; inspect documents before cleanup.'+    return result
skills/fusion-no-modal-dialogs/SKILL.md+24
@@ -178,3 +178,27 @@ is what reached Fusion and what appeared on screen, not what the docstring asser 4. Is there a dismissal sweep doing load-bearing work? If yes, that is the bug. 5. Does any guard swallow its own failure? Make it shout. 6. Test by recording the commands that actually reach Fusion.++## Save checkpoints and close completed scratch documents++After edits, inspect `documentHygiene` in bridge responses. `isSaved` and `isModified`+are separate: a previously saved document can have new edits; a pristine blank Untitled+is not a dirty board. Unknown properties are not proof of a clean state.++At each completed routing/DRC milestone and before ending a task:++1. Save the working document, or export a fresh native checkpoint to a durable task folder.+2. Verify the file exists, has content, and can be reopened; log its path and hash with the+   exact document identity. Exporting/`Document.CopyToDesktop` may leave the live tab+   `isSaved=false, isModified=true`; do not report that as a Fusion save.+3. Close completed documents opened by this task, one exact identity at a time, after+   checkpoint verification. Do not infer ownership from a name prefix, close all tabs,+   delete recovery data, or discard edits in a pre-existing user document.+4. Re-enumerate documents. Leave only ongoing work and pre-existing user documents;+   Fusion may retain one pristine Untitled tab.++If Fusion reports that recovery-file creation failed, checkpoint immediately and then+save or close the affected task document. The warning alone does not establish why+recovery failed; inspect disk/path/permissions if it recurs in a clean session. Never+silence the warning by disabling recovery. The advisory response does not inspect+recovery health and does not automatically save or close documents.
tests/test_document_hygiene.pyadded+28
@@ -0,0 +1,28 @@+import importlib.util+from pathlib import Path+from types import SimpleNamespace as NS+import unittest+spec=importlib.util.spec_from_file_location('hygiene',Path(__file__).parents[1]/'addin/AdomBridge/commands/document_hygiene.py')+m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m)+def app(*docs): return NS(documents=NS(count=len(docs),item=lambda i:docs[i]))+def doc(saved,modified,name='board'): return NS(name=name,isSaved=saved,isModified=modified)+class Hygiene(unittest.TestCase):+ def test_export_does_not_clear_dirty_state(self):+  r=m.attach_document_hygiene(app(doc(False,True)),{'success':True,'_hint':'Existing hint'})+  self.assertEqual(r['documentHygiene']['needsSaveCount'],1)+  self.assertTrue(r['_hint'].startswith('Existing hint'))+  self.assertIn('separate checkpoint',r['_hint'])+ def test_previously_saved_but_modified(self):+  self.assertEqual(m.inspect_documents(app(doc(True,True)))['needsSaveCount'],1)+ def test_blank_untouched_is_not_dirty(self):+  self.assertEqual(m.inspect_documents(app(doc(False,False,'Untitled')))['needsSaveCount'],0)+ def test_unknown_is_not_reported_clean(self):+  r=m.inspect_documents(app(NS(name='unsupported')))+  self.assertFalse(r['documents'][0]['saveStateKnown'])+ def test_errors_and_original_hint_preserved(self):+  r=m.attach_document_hygiene(app(doc(False,True)),{'success':False,'error':'actual failure','_hint':'original'})+  self.assertFalse(r['success']);self.assertEqual(r['error'],'actual failure')+ def test_enumeration_failure_does_not_replace_result(self):+  r=m.attach_document_hygiene(NS(),{'success':True})+  self.assertTrue(r['success']);self.assertFalse(r['documentHygiene']['inspectionComplete'])+if __name__=='__main__':unittest.main()