create-egregore 0.20.2 → 0.21.0-runtime-mvp.2
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.
- package/assets/egregore-launcher.js +1013 -16
- package/bin/cli.js +11 -0
- package/lib/claude-runtime.js +192 -0
- package/lib/local.js +38 -31
- package/lib/pi-runtime.js +1 -0
- package/lib/prime-runtime.js +1 -0
- package/lib/runtime-identity.js +146 -0
- package/lib/setup.js +116 -17
- package/package.json +10 -6
- package/runtime/claude/.claude/context/commit-format.md +110 -0
- package/runtime/claude/.claude/context/git-language.md +46 -0
- package/runtime/claude/.claude/context/notification-consent.md +66 -0
- package/runtime/claude/.claude/context/pr-format.md +67 -0
- package/runtime/claude/.claude/context/telemetry.md +133 -0
- package/runtime/claude/.claude/hooks/boundary-check.sh +333 -0
- package/runtime/claude/.claude/hooks/branch-guard.sh +231 -0
- package/runtime/claude/.claude/hooks/card-context.sh +38 -0
- package/runtime/claude/.claude/hooks/emissary-detect.sh +53 -0
- package/runtime/claude/.claude/hooks/memory-refresh.sh +67 -0
- package/runtime/claude/.claude/hooks/onboarding-guard.sh +86 -0
- package/runtime/claude/.claude/hooks/retrieval-context.sh +43 -0
- package/runtime/claude/.claude/rules/voice-bedrock.md +73 -0
- package/runtime/claude/.claude/settings.json +131 -0
- package/runtime/claude/.claude/skills/activity/SKILL.md +68 -0
- package/runtime/claude/.claude/skills/announce/SKILL.md +54 -0
- package/runtime/claude/.claude/skills/archive/SKILL.md +71 -0
- package/runtime/claude/.claude/skills/branch/SKILL.md +147 -0
- package/runtime/claude/.claude/skills/checkup/SKILL.md +159 -0
- package/runtime/claude/.claude/skills/commit/SKILL.md +103 -0
- package/runtime/claude/.claude/skills/contribute/SKILL.md +343 -0
- package/runtime/claude/.claude/skills/create-skill/SKILL.md +135 -0
- package/runtime/claude/.claude/skills/dark-mode/SKILL.md +410 -0
- package/runtime/claude/.claude/skills/dashboard/SKILL.md +43 -0
- package/runtime/claude/.claude/skills/deep-reflect/REPORT.md +140 -0
- package/runtime/claude/.claude/skills/deep-reflect/RESEARCH.md +132 -0
- package/runtime/claude/.claude/skills/deep-reflect/ROLES.md +104 -0
- package/runtime/claude/.claude/skills/deep-reflect/SKILL.md +170 -0
- package/runtime/claude/.claude/skills/delete-user/SKILL.md +150 -0
- package/runtime/claude/.claude/skills/emissary/SKILL.md +220 -0
- package/runtime/claude/.claude/skills/handoff/SKILL.md +124 -0
- package/runtime/claude/.claude/skills/harvest/AUDIT.md +546 -0
- package/runtime/claude/.claude/skills/harvest/FORMAT.md +436 -0
- package/runtime/claude/.claude/skills/harvest/PROCESS.md +395 -0
- package/runtime/claude/.claude/skills/harvest/QUESTION_PALETTE.md +76 -0
- package/runtime/claude/.claude/skills/harvest/SKILL.md +100 -0
- package/runtime/claude/.claude/skills/ingest/SKILL.md +112 -0
- package/runtime/claude/.claude/skills/ingest-corpus/SKILL.md +187 -0
- package/runtime/claude/.claude/skills/ingest-notion/SKILL.md +214 -0
- package/runtime/claude/.claude/skills/ingest-user-interview/SKILL.md +65 -0
- package/runtime/claude/.claude/skills/ingest-user-interview/references/analysis-contract.md +125 -0
- package/runtime/claude/.claude/skills/invite/SKILL.md +111 -0
- package/runtime/claude/.claude/skills/issue/SKILL.md +104 -0
- package/runtime/claude/.claude/skills/me/SKILL.md +81 -0
- package/runtime/claude/.claude/skills/meeting/SKILL.md +76 -0
- package/runtime/claude/.claude/skills/meeting/references/analysis-contract.md +102 -0
- package/runtime/claude/.claude/skills/note/SKILL.md +71 -0
- package/runtime/claude/.claude/skills/notion-connect/SKILL.md +124 -0
- package/runtime/claude/.claude/skills/onboarding/SKILL.md +159 -0
- package/runtime/claude/.claude/skills/pr/SKILL.md +85 -0
- package/runtime/claude/.claude/skills/pull/SKILL.md +79 -0
- package/runtime/claude/.claude/skills/push/SKILL.md +47 -0
- package/runtime/claude/.claude/skills/quest/SKILL.md +121 -0
- package/runtime/claude/.claude/skills/reflect/SKILL.md +69 -0
- package/runtime/claude/.claude/skills/review-pr/SKILL.md +276 -0
- package/runtime/claude/.claude/skills/save/SKILL.md +92 -0
- package/runtime/claude/.claude/skills/slack-connect/SKILL.md +148 -0
- package/runtime/claude/.claude/skills/summon/SKILL.md +240 -0
- package/runtime/claude/.claude/skills/sync-repos/SKILL.md +82 -0
- package/runtime/claude/.claude/skills/teams-connect/SKILL.md +216 -0
- package/runtime/claude/.claude/skills/telegram-connect/SKILL.md +110 -0
- package/runtime/claude/.claude/skills/telemetry/SKILL.md +117 -0
- package/runtime/claude/.claude/skills/test/SKILL.md +87 -0
- package/runtime/claude/.claude/skills/the-spiral/SKILL.md +492 -0
- package/runtime/claude/.claude/skills/tui-design/SKILL.md +188 -0
- package/runtime/claude/.claude/skills/tutorial/SKILL.md +100 -0
- package/runtime/claude/.claude/skills/update/SKILL.md +265 -0
- package/runtime/claude/.claude/skills/view/SKILL.md +393 -0
- package/runtime/claude/.claude/skills/view/compose-scaffold.html +221 -0
- package/runtime/claude/.claude/skills/visual-explain/SKILL.md +132 -0
- package/runtime/claude/.claude/skills/wrap/SKILL.md +98 -0
- package/runtime/claude/CLAUDE.md +447 -0
- package/runtime/claude/bin/activity-action.sh +92 -0
- package/runtime/claude/bin/activity-data.sh +17 -0
- package/runtime/claude/bin/agent.sh +783 -0
- package/runtime/claude/bin/artifact-register.sh +164 -0
- package/runtime/claude/bin/artifact-writeback.sh +53 -0
- package/runtime/claude/bin/artifacts.sh +288 -0
- package/runtime/claude/bin/attendant.sh +707 -0
- package/runtime/claude/bin/autosave.sh +112 -0
- package/runtime/claude/bin/boundary.sh +253 -0
- package/runtime/claude/bin/build-sealed-dataroom.mjs +722 -0
- package/runtime/claude/bin/capture-reconcile.sh +69 -0
- package/runtime/claude/bin/capture-run.sh +357 -0
- package/runtime/claude/bin/codex-install-skills.sh +78 -0
- package/runtime/claude/bin/codex-render-spec.mjs +462 -0
- package/runtime/claude/bin/codex-session-start.sh +275 -0
- package/runtime/claude/bin/codex-skill-render.mjs +349 -0
- package/runtime/claude/bin/codex-sync-skills.sh +237 -0
- package/runtime/claude/bin/connect-refresh.sh +33 -0
- package/runtime/claude/bin/contribute-guard.sh +79 -0
- package/runtime/claude/bin/corpus_map.py +806 -0
- package/runtime/claude/bin/corpus_survey.py +304 -0
- package/runtime/claude/bin/dashboard-data.sh +18 -0
- package/runtime/claude/bin/github-auth.sh +121 -0
- package/runtime/claude/bin/graph-batch.sh +86 -0
- package/runtime/claude/bin/graph-op.sh +1106 -0
- package/runtime/claude/bin/graph-projection.sh +54 -0
- package/runtime/claude/bin/graph-wal.sh +242 -0
- package/runtime/claude/bin/graph.sh +267 -0
- package/runtime/claude/bin/handoff-lifecycle-job.sh +146 -0
- package/runtime/claude/bin/handoff-lifecycle.sh +280 -0
- package/runtime/claude/bin/handoff-pr-backfill.sh +86 -0
- package/runtime/claude/bin/handoff-preview.sh +169 -0
- package/runtime/claude/bin/handoff-run.sh +692 -0
- package/runtime/claude/bin/handoff-save-egregore.sh +224 -0
- package/runtime/claude/bin/index-handoff.sh +322 -0
- package/runtime/claude/bin/ingest-graph.sh +86 -0
- package/runtime/claude/bin/ingest-item.sh +10 -0
- package/runtime/claude/bin/ingest-surface.html +495 -0
- package/runtime/claude/bin/ingest.py +1011 -0
- package/runtime/claude/bin/ingest.sh +308 -0
- package/runtime/claude/bin/ingest_graph.py +856 -0
- package/runtime/claude/bin/ingest_surface.py +544 -0
- package/runtime/claude/bin/invite.sh +6 -0
- package/runtime/claude/bin/issue.sh +10 -0
- package/runtime/claude/bin/knowledge.sh +10 -0
- package/runtime/claude/bin/lib/artifact-id.sh +46 -0
- package/runtime/claude/bin/lib/config.sh +136 -0
- package/runtime/claude/bin/lib/context.sh +642 -0
- package/runtime/claude/bin/lib/dashboard-artifact.sh +242 -0
- package/runtime/claude/bin/lib/ensure-develop.sh +183 -0
- package/runtime/claude/bin/lib/git-message.sh +37 -0
- package/runtime/claude/bin/lib/git-sync.sh +494 -0
- package/runtime/claude/bin/lib/greeting.sh +548 -0
- package/runtime/claude/bin/lib/handoff-meta.sh +97 -0
- package/runtime/claude/bin/lib/hash.sh +13 -0
- package/runtime/claude/bin/lib/identity.sh +213 -0
- package/runtime/claude/bin/lib/metrics.sh +374 -0
- package/runtime/claude/bin/lib/noncode.sh +31 -0
- package/runtime/claude/bin/lib/permissions.sh +106 -0
- package/runtime/claude/bin/lib/time.sh +56 -0
- package/runtime/claude/bin/lib/worktree-links.sh +85 -0
- package/runtime/claude/bin/node-run.sh +42 -0
- package/runtime/claude/bin/notification.sh +6 -0
- package/runtime/claude/bin/notify.sh +636 -0
- package/runtime/claude/bin/observe-context.sh +14 -0
- package/runtime/claude/bin/onboarding.sh +6 -0
- package/runtime/claude/bin/person.py +634 -0
- package/runtime/claude/bin/person.sh +481 -0
- package/runtime/claude/bin/pi-render-spec.mjs +140 -0
- package/runtime/claude/bin/post-update.sh +37 -0
- package/runtime/claude/bin/prime-render-spec.mjs +147 -0
- package/runtime/claude/bin/publish-artifact.sh +324 -0
- package/runtime/claude/bin/publish-references.sh +144 -0
- package/runtime/claude/bin/quest.sh +9 -0
- package/runtime/claude/bin/question.sh +8 -0
- package/runtime/claude/bin/repo-state.sh +96 -0
- package/runtime/claude/bin/research-ingest.sh +7 -0
- package/runtime/claude/bin/restore-owned-skills.sh +67 -0
- package/runtime/claude/bin/retrieval_eval.py +385 -0
- package/runtime/claude/bin/runtime-identity.sh +70 -0
- package/runtime/claude/bin/runtime-upgrade.sh +43 -0
- package/runtime/claude/bin/scroll.sh +9 -0
- package/runtime/claude/bin/search.sh +318 -0
- package/runtime/claude/bin/session-autosave.sh +220 -0
- package/runtime/claude/bin/session-log.sh +199 -0
- package/runtime/claude/bin/session-start.sh +696 -0
- package/runtime/claude/bin/settings.sh +532 -0
- package/runtime/claude/bin/startup-check.sh +136 -0
- package/runtime/claude/bin/statusline.sh +41 -0
- package/runtime/claude/bin/telemetry.sh +666 -0
- package/runtime/claude/bin/test-changes.sh +344 -0
- package/runtime/claude/bin/thread.sh +9 -0
- package/runtime/claude/bin/todo.sh +9 -0
- package/runtime/claude/bin/view-render.sh +126 -0
- package/runtime/claude/bin/worktree-create.sh +145 -0
- package/runtime/claude/bin/worktree-remove.sh +50 -0
- package/runtime/claude/bin/worktree.sh +198 -0
- package/runtime/claude/egregore_runtime/__init__.py +228 -0
- package/runtime/claude/egregore_runtime/adapters/__init__.py +5 -0
- package/runtime/claude/egregore_runtime/adapters/invitation.py +237 -0
- package/runtime/claude/egregore_runtime/adapters/notification.py +59 -0
- package/runtime/claude/egregore_runtime/adapters/qmd.py +1701 -0
- package/runtime/claude/egregore_runtime/adapters/qmd_cli.py +210 -0
- package/runtime/claude/egregore_runtime/adapters/qmd_embed_worker.py +59 -0
- package/runtime/claude/egregore_runtime/admin_gate.py +102 -0
- package/runtime/claude/egregore_runtime/artifacts.py +523 -0
- package/runtime/claude/egregore_runtime/contracts.py +499 -0
- package/runtime/claude/egregore_runtime/entitlements.py +85 -0
- package/runtime/claude/egregore_runtime/errors.py +10 -0
- package/runtime/claude/egregore_runtime/graph.py +356 -0
- package/runtime/claude/egregore_runtime/handoff_lookup.py +196 -0
- package/runtime/claude/egregore_runtime/harness_cli.py +1221 -0
- package/runtime/claude/egregore_runtime/identity.py +734 -0
- package/runtime/claude/egregore_runtime/identity_cli.py +91 -0
- package/runtime/claude/egregore_runtime/ingest.py +575 -0
- package/runtime/claude/egregore_runtime/ingest_cli.py +254 -0
- package/runtime/claude/egregore_runtime/ingest_index_cli.py +147 -0
- package/runtime/claude/egregore_runtime/invitation_cli.py +51 -0
- package/runtime/claude/egregore_runtime/invitations.py +116 -0
- package/runtime/claude/egregore_runtime/issue_cli.py +305 -0
- package/runtime/claude/egregore_runtime/issues.py +359 -0
- package/runtime/claude/egregore_runtime/knowledge.py +376 -0
- package/runtime/claude/egregore_runtime/knowledge_cli.py +173 -0
- package/runtime/claude/egregore_runtime/lifecycle.py +888 -0
- package/runtime/claude/egregore_runtime/lifecycle_cli.py +269 -0
- package/runtime/claude/egregore_runtime/notification_cli.py +125 -0
- package/runtime/claude/egregore_runtime/notifications.py +230 -0
- package/runtime/claude/egregore_runtime/observe.py +342 -0
- package/runtime/claude/egregore_runtime/onboarding.py +290 -0
- package/runtime/claude/egregore_runtime/onboarding_cli.py +110 -0
- package/runtime/claude/egregore_runtime/policy.py +144 -0
- package/runtime/claude/egregore_runtime/profile.py +193 -0
- package/runtime/claude/egregore_runtime/quest_cli.py +216 -0
- package/runtime/claude/egregore_runtime/question_cli.py +180 -0
- package/runtime/claude/egregore_runtime/questions.py +304 -0
- package/runtime/claude/egregore_runtime/quests.py +419 -0
- package/runtime/claude/egregore_runtime/readiness.py +379 -0
- package/runtime/claude/egregore_runtime/research_ingest.py +256 -0
- package/runtime/claude/egregore_runtime/research_ingest_cli.py +44 -0
- package/runtime/claude/egregore_runtime/runtime.py +464 -0
- package/runtime/claude/egregore_runtime/scroll_cli.py +185 -0
- package/runtime/claude/egregore_runtime/scrolls.py +726 -0
- package/runtime/claude/egregore_runtime/status.py +412 -0
- package/runtime/claude/egregore_runtime/status_cli.py +174 -0
- package/runtime/claude/egregore_runtime/sync.py +218 -0
- package/runtime/claude/egregore_runtime/teamsync.py +139 -0
- package/runtime/claude/egregore_runtime/telemetry.py +640 -0
- package/runtime/claude/egregore_runtime/thread_cli.py +173 -0
- package/runtime/claude/egregore_runtime/threads.py +517 -0
- package/runtime/claude/egregore_runtime/todo_cli.py +142 -0
- package/runtime/claude/egregore_runtime/todos.py +244 -0
- package/runtime/claude/egregore_runtime/upgrade.py +770 -0
- package/runtime/claude/egregore_runtime/upgrade_worker.py +53 -0
- package/runtime/claude/egregore_runtime/writeback.py +391 -0
- package/runtime/claude/egregore_runtime/writeback_cli.py +114 -0
- package/runtime/claude/manifest.json +284 -0
- package/runtime/codex/.codex/hooks/search-hint.js +21 -52
- package/runtime/codex/.codex/hooks.json +3 -3
- package/runtime/codex/.codex/skills/activity/SKILL.md +23 -66
- package/runtime/codex/.codex/skills/announce/SKILL.md +27 -50
- package/runtime/codex/.codex/skills/archive/SKILL.md +21 -36
- package/runtime/codex/.codex/skills/checkup/SKILL.md +77 -36
- package/runtime/codex/.codex/skills/dark-mode/SKILL.md +1 -1
- package/runtime/codex/.codex/skills/dashboard/SKILL.md +17 -47
- package/runtime/codex/.codex/skills/handoff/SKILL.md +84 -133
- package/runtime/codex/.codex/skills/harvest/SKILL.md +32 -117
- package/runtime/codex/.codex/skills/ingest/SKILL.md +47 -25
- package/runtime/codex/.codex/skills/ingest-user-interview/SKILL.md +18 -36
- package/runtime/codex/.codex/skills/ingest-user-interview/references/analysis-contract.md +42 -0
- package/runtime/codex/.codex/skills/invite/SKILL.md +21 -35
- package/runtime/codex/.codex/skills/issue/SKILL.md +60 -41
- package/runtime/codex/.codex/skills/me/SKILL.md +1 -1
- package/runtime/codex/.codex/skills/meeting/SKILL.md +16 -36
- package/runtime/codex/.codex/skills/meeting/references/analysis-contract.md +48 -0
- package/runtime/codex/.codex/skills/note/SKILL.md +30 -25
- package/runtime/codex/.codex/skills/onboarding/SKILL.md +36 -41
- package/runtime/codex/.codex/skills/pull/SKILL.md +1 -1
- package/runtime/codex/.codex/skills/quest/SKILL.md +27 -66
- package/runtime/codex/.codex/skills/reflect/SKILL.md +22 -36
- package/runtime/codex/.codex/skills/save/SKILL.md +2 -2
- package/runtime/codex/.codex/skills/telemetry/SKILL.md +1 -1
- package/runtime/codex/.codex/skills/test/SKILL.md +1 -1
- package/runtime/codex/.codex/skills/tutorial/SKILL.md +40 -41
- package/runtime/codex/.codex/skills/view/SKILL.md +115 -111
- package/runtime/codex/.codex/skills/wrap/SKILL.md +45 -41
- package/runtime/codex/.codex/spec-manifest.json +9 -9
- package/runtime/codex/AGENTS.md +62 -58
- package/runtime/codex/bin/activity-action.sh +92 -0
- package/runtime/codex/bin/activity-data.sh +12 -233
- package/runtime/codex/bin/agent.sh +84 -100
- package/runtime/codex/bin/artifact-register.sh +164 -0
- package/runtime/codex/bin/artifact-writeback.sh +53 -0
- package/runtime/codex/bin/artifacts.sh +288 -0
- package/runtime/codex/bin/build-sealed-dataroom.mjs +722 -0
- package/runtime/codex/bin/capture-run.sh +18 -2
- package/runtime/codex/bin/codex-render-spec.mjs +41 -16
- package/runtime/codex/bin/codex-session-start.sh +14 -4
- package/runtime/codex/bin/codex-skill-render.mjs +9 -7
- package/runtime/codex/bin/corpus_map.py +806 -0
- package/runtime/codex/bin/corpus_survey.py +304 -0
- package/runtime/codex/bin/dashboard-data.sh +11 -171
- package/runtime/codex/bin/github-auth.sh +121 -0
- package/runtime/codex/bin/graph-batch.sh +7 -0
- package/runtime/codex/bin/graph-op.sh +4 -0
- package/runtime/codex/bin/graph-projection.sh +54 -0
- package/runtime/codex/bin/graph-wal.sh +13 -0
- package/runtime/codex/bin/graph.sh +44 -0
- package/runtime/codex/bin/handoff-lifecycle-job.sh +146 -0
- package/runtime/codex/bin/handoff-lifecycle.sh +280 -0
- package/runtime/codex/bin/handoff-preview.sh +169 -0
- package/runtime/codex/bin/handoff-run.sh +42 -26
- package/runtime/codex/bin/ingest-graph.sh +6 -0
- package/runtime/codex/bin/ingest-item.sh +10 -0
- package/runtime/codex/bin/ingest.sh +59 -30
- package/runtime/codex/bin/invite.sh +6 -0
- package/runtime/codex/bin/issue.sh +10 -0
- package/runtime/codex/bin/knowledge.sh +10 -0
- package/runtime/codex/bin/lib/config.sh +20 -0
- package/runtime/codex/bin/lib/context.sh +46 -25
- package/runtime/codex/bin/lib/git-sync.sh +70 -6
- package/runtime/codex/bin/lib/greeting.sh +34 -5
- package/runtime/codex/bin/lib/handoff-meta.sh +97 -0
- package/runtime/codex/bin/lib/worktree-links.sh +6 -0
- package/runtime/codex/bin/node-run.sh +42 -0
- package/runtime/codex/bin/notification.sh +6 -0
- package/runtime/codex/bin/observe-context.sh +14 -0
- package/runtime/codex/bin/onboarding.sh +6 -0
- package/runtime/codex/bin/person.py +4 -0
- package/runtime/codex/bin/person.sh +43 -10
- package/runtime/codex/bin/pi-render-spec.mjs +140 -0
- package/runtime/codex/bin/post-update.sh +37 -0
- package/runtime/codex/bin/prime-render-spec.mjs +147 -0
- package/runtime/codex/bin/publish-artifact.sh +55 -2
- package/runtime/codex/bin/quest.sh +9 -0
- package/runtime/codex/bin/question.sh +8 -0
- package/runtime/codex/bin/research-ingest.sh +7 -0
- package/runtime/codex/bin/restore-owned-skills.sh +67 -0
- package/runtime/codex/bin/retrieval_eval.py +385 -0
- package/runtime/codex/bin/runtime-identity.sh +70 -0
- package/runtime/codex/bin/runtime-upgrade.sh +43 -0
- package/runtime/codex/bin/scroll.sh +9 -0
- package/runtime/codex/bin/search.sh +231 -195
- package/runtime/codex/bin/settings.sh +174 -5
- package/runtime/codex/bin/startup-check.sh +136 -0
- package/runtime/codex/bin/telemetry.sh +522 -247
- package/runtime/codex/bin/test-changes.sh +344 -0
- package/runtime/codex/bin/thread.sh +9 -0
- package/runtime/codex/bin/todo.sh +9 -0
- package/runtime/codex/bin/view-render.sh +126 -0
- package/runtime/codex/bin/worktree-create.sh +19 -5
- package/runtime/codex/egregore_runtime/__init__.py +228 -0
- package/runtime/codex/egregore_runtime/adapters/__init__.py +5 -0
- package/runtime/codex/egregore_runtime/adapters/invitation.py +237 -0
- package/runtime/codex/egregore_runtime/adapters/notification.py +59 -0
- package/runtime/codex/egregore_runtime/adapters/qmd.py +1701 -0
- package/runtime/codex/egregore_runtime/adapters/qmd_cli.py +210 -0
- package/runtime/codex/egregore_runtime/adapters/qmd_embed_worker.py +59 -0
- package/runtime/codex/egregore_runtime/admin_gate.py +102 -0
- package/runtime/codex/egregore_runtime/artifacts.py +523 -0
- package/runtime/codex/egregore_runtime/contracts.py +499 -0
- package/runtime/codex/egregore_runtime/entitlements.py +85 -0
- package/runtime/codex/egregore_runtime/errors.py +10 -0
- package/runtime/codex/egregore_runtime/graph.py +356 -0
- package/runtime/codex/egregore_runtime/handoff_lookup.py +196 -0
- package/runtime/codex/egregore_runtime/harness_cli.py +1221 -0
- package/runtime/codex/egregore_runtime/identity.py +734 -0
- package/runtime/codex/egregore_runtime/identity_cli.py +91 -0
- package/runtime/codex/egregore_runtime/ingest.py +575 -0
- package/runtime/codex/egregore_runtime/ingest_cli.py +254 -0
- package/runtime/codex/egregore_runtime/ingest_index_cli.py +147 -0
- package/runtime/codex/egregore_runtime/invitation_cli.py +51 -0
- package/runtime/codex/egregore_runtime/invitations.py +116 -0
- package/runtime/codex/egregore_runtime/issue_cli.py +305 -0
- package/runtime/codex/egregore_runtime/issues.py +359 -0
- package/runtime/codex/egregore_runtime/knowledge.py +376 -0
- package/runtime/codex/egregore_runtime/knowledge_cli.py +173 -0
- package/runtime/codex/egregore_runtime/lifecycle.py +888 -0
- package/runtime/codex/egregore_runtime/lifecycle_cli.py +269 -0
- package/runtime/codex/egregore_runtime/notification_cli.py +125 -0
- package/runtime/codex/egregore_runtime/notifications.py +230 -0
- package/runtime/codex/egregore_runtime/observe.py +342 -0
- package/runtime/codex/egregore_runtime/onboarding.py +290 -0
- package/runtime/codex/egregore_runtime/onboarding_cli.py +110 -0
- package/runtime/codex/egregore_runtime/policy.py +144 -0
- package/runtime/codex/egregore_runtime/profile.py +193 -0
- package/runtime/codex/egregore_runtime/quest_cli.py +216 -0
- package/runtime/codex/egregore_runtime/question_cli.py +180 -0
- package/runtime/codex/egregore_runtime/questions.py +304 -0
- package/runtime/codex/egregore_runtime/quests.py +419 -0
- package/runtime/codex/egregore_runtime/readiness.py +379 -0
- package/runtime/codex/egregore_runtime/research_ingest.py +256 -0
- package/runtime/codex/egregore_runtime/research_ingest_cli.py +44 -0
- package/runtime/codex/egregore_runtime/runtime.py +464 -0
- package/runtime/codex/egregore_runtime/scroll_cli.py +185 -0
- package/runtime/codex/egregore_runtime/scrolls.py +726 -0
- package/runtime/codex/egregore_runtime/status.py +412 -0
- package/runtime/codex/egregore_runtime/status_cli.py +174 -0
- package/runtime/codex/egregore_runtime/sync.py +218 -0
- package/runtime/codex/egregore_runtime/teamsync.py +139 -0
- package/runtime/codex/egregore_runtime/telemetry.py +640 -0
- package/runtime/codex/egregore_runtime/thread_cli.py +173 -0
- package/runtime/codex/egregore_runtime/threads.py +517 -0
- package/runtime/codex/egregore_runtime/todo_cli.py +142 -0
- package/runtime/codex/egregore_runtime/todos.py +244 -0
- package/runtime/codex/egregore_runtime/upgrade.py +770 -0
- package/runtime/codex/egregore_runtime/upgrade_worker.py +53 -0
- package/runtime/codex/egregore_runtime/writeback.py +391 -0
- package/runtime/codex/egregore_runtime/writeback_cli.py +114 -0
- package/runtime/codex/manifest.json +98 -1
- package/runtime/pi/.codex/hooks/search-hint.js +21 -52
- package/runtime/pi/.codex/hooks.json +3 -3
- package/runtime/pi/.codex/skills/activity/SKILL.md +23 -66
- package/runtime/pi/.codex/skills/announce/SKILL.md +27 -50
- package/runtime/pi/.codex/skills/archive/SKILL.md +21 -36
- package/runtime/pi/.codex/skills/checkup/SKILL.md +77 -36
- package/runtime/pi/.codex/skills/dark-mode/SKILL.md +1 -1
- package/runtime/pi/.codex/skills/dashboard/SKILL.md +17 -47
- package/runtime/pi/.codex/skills/handoff/SKILL.md +84 -133
- package/runtime/pi/.codex/skills/harvest/SKILL.md +32 -117
- package/runtime/pi/.codex/skills/ingest/SKILL.md +47 -25
- package/runtime/pi/.codex/skills/ingest-user-interview/SKILL.md +18 -36
- package/runtime/pi/.codex/skills/ingest-user-interview/references/analysis-contract.md +42 -0
- package/runtime/pi/.codex/skills/invite/SKILL.md +21 -35
- package/runtime/pi/.codex/skills/issue/SKILL.md +60 -41
- package/runtime/pi/.codex/skills/me/SKILL.md +1 -1
- package/runtime/pi/.codex/skills/meeting/SKILL.md +16 -36
- package/runtime/pi/.codex/skills/meeting/references/analysis-contract.md +48 -0
- package/runtime/pi/.codex/skills/note/SKILL.md +30 -25
- package/runtime/pi/.codex/skills/onboarding/SKILL.md +36 -41
- package/runtime/pi/.codex/skills/pull/SKILL.md +1 -1
- package/runtime/pi/.codex/skills/quest/SKILL.md +27 -66
- package/runtime/pi/.codex/skills/reflect/SKILL.md +22 -36
- package/runtime/pi/.codex/skills/save/SKILL.md +2 -2
- package/runtime/pi/.codex/skills/telemetry/SKILL.md +1 -1
- package/runtime/pi/.codex/skills/test/SKILL.md +1 -1
- package/runtime/pi/.codex/skills/tutorial/SKILL.md +40 -41
- package/runtime/pi/.codex/skills/view/SKILL.md +115 -111
- package/runtime/pi/.codex/skills/wrap/SKILL.md +45 -41
- package/runtime/pi/.codex/spec-manifest.json +9 -9
- package/runtime/pi/.pi/APPEND_SYSTEM.md +57 -48
- package/runtime/pi/.pi/extensions/egregore.ts +7 -0
- package/runtime/pi/.pi/spec-manifest.json +2 -2
- package/runtime/pi/AGENTS.md +62 -58
- package/runtime/pi/bin/activity-action.sh +92 -0
- package/runtime/pi/bin/activity-data.sh +12 -233
- package/runtime/pi/bin/agent.sh +84 -100
- package/runtime/pi/bin/artifact-register.sh +164 -0
- package/runtime/pi/bin/artifact-writeback.sh +53 -0
- package/runtime/pi/bin/artifacts.sh +288 -0
- package/runtime/pi/bin/build-sealed-dataroom.mjs +722 -0
- package/runtime/pi/bin/capture-run.sh +18 -2
- package/runtime/pi/bin/codex-render-spec.mjs +41 -16
- package/runtime/pi/bin/codex-session-start.sh +14 -4
- package/runtime/pi/bin/codex-skill-render.mjs +9 -7
- package/runtime/pi/bin/corpus_map.py +806 -0
- package/runtime/pi/bin/corpus_survey.py +304 -0
- package/runtime/pi/bin/dashboard-data.sh +11 -171
- package/runtime/pi/bin/github-auth.sh +121 -0
- package/runtime/pi/bin/graph-batch.sh +7 -0
- package/runtime/pi/bin/graph-op.sh +4 -0
- package/runtime/pi/bin/graph-projection.sh +54 -0
- package/runtime/pi/bin/graph-wal.sh +13 -0
- package/runtime/pi/bin/graph.sh +44 -0
- package/runtime/pi/bin/handoff-lifecycle-job.sh +146 -0
- package/runtime/pi/bin/handoff-lifecycle.sh +280 -0
- package/runtime/pi/bin/handoff-preview.sh +169 -0
- package/runtime/pi/bin/handoff-run.sh +42 -26
- package/runtime/pi/bin/ingest-graph.sh +6 -0
- package/runtime/pi/bin/ingest-item.sh +10 -0
- package/runtime/pi/bin/ingest.sh +59 -30
- package/runtime/pi/bin/invite.sh +6 -0
- package/runtime/pi/bin/issue.sh +10 -0
- package/runtime/pi/bin/knowledge.sh +10 -0
- package/runtime/pi/bin/lib/config.sh +20 -0
- package/runtime/pi/bin/lib/context.sh +46 -25
- package/runtime/pi/bin/lib/git-sync.sh +70 -6
- package/runtime/pi/bin/lib/greeting.sh +34 -5
- package/runtime/pi/bin/lib/handoff-meta.sh +97 -0
- package/runtime/pi/bin/lib/worktree-links.sh +6 -0
- package/runtime/pi/bin/node-run.sh +42 -0
- package/runtime/pi/bin/notification.sh +6 -0
- package/runtime/pi/bin/observe-context.sh +14 -0
- package/runtime/pi/bin/onboarding.sh +6 -0
- package/runtime/pi/bin/person.py +4 -0
- package/runtime/pi/bin/person.sh +43 -10
- package/runtime/pi/bin/pi-observe-context.mjs +93 -0
- package/runtime/pi/bin/pi-product-workflows.mjs +1 -1
- package/runtime/pi/bin/pi-render-spec.mjs +1 -1
- package/runtime/pi/bin/post-update.sh +37 -0
- package/runtime/pi/bin/prime-render-spec.mjs +147 -0
- package/runtime/pi/bin/publish-artifact.sh +55 -2
- package/runtime/pi/bin/quest.sh +9 -0
- package/runtime/pi/bin/question.sh +8 -0
- package/runtime/pi/bin/research-ingest.sh +7 -0
- package/runtime/pi/bin/restore-owned-skills.sh +67 -0
- package/runtime/pi/bin/retrieval_eval.py +385 -0
- package/runtime/pi/bin/runtime-identity.sh +70 -0
- package/runtime/pi/bin/runtime-upgrade.sh +43 -0
- package/runtime/pi/bin/scroll.sh +9 -0
- package/runtime/pi/bin/search.sh +231 -195
- package/runtime/pi/bin/settings.sh +174 -5
- package/runtime/pi/bin/startup-check.sh +136 -0
- package/runtime/pi/bin/telemetry.sh +522 -247
- package/runtime/pi/bin/test-changes.sh +344 -0
- package/runtime/pi/bin/thread.sh +9 -0
- package/runtime/pi/bin/todo.sh +9 -0
- package/runtime/pi/bin/view-render.sh +126 -0
- package/runtime/pi/bin/worktree-create.sh +19 -5
- package/runtime/pi/egregore_runtime/__init__.py +228 -0
- package/runtime/pi/egregore_runtime/adapters/__init__.py +5 -0
- package/runtime/pi/egregore_runtime/adapters/invitation.py +237 -0
- package/runtime/pi/egregore_runtime/adapters/notification.py +59 -0
- package/runtime/pi/egregore_runtime/adapters/qmd.py +1701 -0
- package/runtime/pi/egregore_runtime/adapters/qmd_cli.py +210 -0
- package/runtime/pi/egregore_runtime/adapters/qmd_embed_worker.py +59 -0
- package/runtime/pi/egregore_runtime/admin_gate.py +102 -0
- package/runtime/pi/egregore_runtime/artifacts.py +523 -0
- package/runtime/pi/egregore_runtime/contracts.py +499 -0
- package/runtime/pi/egregore_runtime/entitlements.py +85 -0
- package/runtime/pi/egregore_runtime/errors.py +10 -0
- package/runtime/pi/egregore_runtime/graph.py +356 -0
- package/runtime/pi/egregore_runtime/handoff_lookup.py +196 -0
- package/runtime/pi/egregore_runtime/harness_cli.py +1221 -0
- package/runtime/pi/egregore_runtime/identity.py +734 -0
- package/runtime/pi/egregore_runtime/identity_cli.py +91 -0
- package/runtime/pi/egregore_runtime/ingest.py +575 -0
- package/runtime/pi/egregore_runtime/ingest_cli.py +254 -0
- package/runtime/pi/egregore_runtime/ingest_index_cli.py +147 -0
- package/runtime/pi/egregore_runtime/invitation_cli.py +51 -0
- package/runtime/pi/egregore_runtime/invitations.py +116 -0
- package/runtime/pi/egregore_runtime/issue_cli.py +305 -0
- package/runtime/pi/egregore_runtime/issues.py +359 -0
- package/runtime/pi/egregore_runtime/knowledge.py +376 -0
- package/runtime/pi/egregore_runtime/knowledge_cli.py +173 -0
- package/runtime/pi/egregore_runtime/lifecycle.py +888 -0
- package/runtime/pi/egregore_runtime/lifecycle_cli.py +269 -0
- package/runtime/pi/egregore_runtime/notification_cli.py +125 -0
- package/runtime/pi/egregore_runtime/notifications.py +230 -0
- package/runtime/pi/egregore_runtime/observe.py +342 -0
- package/runtime/pi/egregore_runtime/onboarding.py +290 -0
- package/runtime/pi/egregore_runtime/onboarding_cli.py +110 -0
- package/runtime/pi/egregore_runtime/policy.py +144 -0
- package/runtime/pi/egregore_runtime/profile.py +193 -0
- package/runtime/pi/egregore_runtime/quest_cli.py +216 -0
- package/runtime/pi/egregore_runtime/question_cli.py +180 -0
- package/runtime/pi/egregore_runtime/questions.py +304 -0
- package/runtime/pi/egregore_runtime/quests.py +419 -0
- package/runtime/pi/egregore_runtime/readiness.py +379 -0
- package/runtime/pi/egregore_runtime/research_ingest.py +256 -0
- package/runtime/pi/egregore_runtime/research_ingest_cli.py +44 -0
- package/runtime/pi/egregore_runtime/runtime.py +464 -0
- package/runtime/pi/egregore_runtime/scroll_cli.py +185 -0
- package/runtime/pi/egregore_runtime/scrolls.py +726 -0
- package/runtime/pi/egregore_runtime/status.py +412 -0
- package/runtime/pi/egregore_runtime/status_cli.py +174 -0
- package/runtime/pi/egregore_runtime/sync.py +218 -0
- package/runtime/pi/egregore_runtime/teamsync.py +139 -0
- package/runtime/pi/egregore_runtime/telemetry.py +640 -0
- package/runtime/pi/egregore_runtime/thread_cli.py +173 -0
- package/runtime/pi/egregore_runtime/threads.py +517 -0
- package/runtime/pi/egregore_runtime/todo_cli.py +142 -0
- package/runtime/pi/egregore_runtime/todos.py +244 -0
- package/runtime/pi/egregore_runtime/upgrade.py +770 -0
- package/runtime/pi/egregore_runtime/upgrade_worker.py +53 -0
- package/runtime/pi/egregore_runtime/writeback.py +391 -0
- package/runtime/pi/egregore_runtime/writeback_cli.py +114 -0
- package/runtime/pi/manifest.json +98 -1
- package/runtime/prime/.codex/hooks/search-hint.js +21 -52
- package/runtime/prime/.codex/hooks.json +3 -3
- package/runtime/prime/.codex/skills/activity/SKILL.md +23 -66
- package/runtime/prime/.codex/skills/announce/SKILL.md +27 -50
- package/runtime/prime/.codex/skills/archive/SKILL.md +21 -36
- package/runtime/prime/.codex/skills/checkup/SKILL.md +77 -36
- package/runtime/prime/.codex/skills/dark-mode/SKILL.md +1 -1
- package/runtime/prime/.codex/skills/dashboard/SKILL.md +17 -47
- package/runtime/prime/.codex/skills/handoff/SKILL.md +84 -133
- package/runtime/prime/.codex/skills/harvest/SKILL.md +32 -117
- package/runtime/prime/.codex/skills/ingest/SKILL.md +47 -25
- package/runtime/prime/.codex/skills/ingest-user-interview/SKILL.md +18 -36
- package/runtime/prime/.codex/skills/ingest-user-interview/references/analysis-contract.md +42 -0
- package/runtime/prime/.codex/skills/invite/SKILL.md +21 -35
- package/runtime/prime/.codex/skills/issue/SKILL.md +60 -41
- package/runtime/prime/.codex/skills/me/SKILL.md +1 -1
- package/runtime/prime/.codex/skills/meeting/SKILL.md +16 -36
- package/runtime/prime/.codex/skills/meeting/references/analysis-contract.md +48 -0
- package/runtime/prime/.codex/skills/note/SKILL.md +30 -25
- package/runtime/prime/.codex/skills/onboarding/SKILL.md +36 -41
- package/runtime/prime/.codex/skills/pull/SKILL.md +1 -1
- package/runtime/prime/.codex/skills/quest/SKILL.md +27 -66
- package/runtime/prime/.codex/skills/reflect/SKILL.md +22 -36
- package/runtime/prime/.codex/skills/save/SKILL.md +2 -2
- package/runtime/prime/.codex/skills/telemetry/SKILL.md +1 -1
- package/runtime/prime/.codex/skills/test/SKILL.md +1 -1
- package/runtime/prime/.codex/skills/tutorial/SKILL.md +40 -41
- package/runtime/prime/.codex/skills/view/SKILL.md +115 -111
- package/runtime/prime/.codex/skills/wrap/SKILL.md +45 -41
- package/runtime/prime/.codex/spec-manifest.json +9 -9
- package/runtime/prime/.prime/agent/APPEND_SYSTEM.md +57 -48
- package/runtime/prime/.prime/agent/extensions/egregore.ts +7 -0
- package/runtime/prime/.prime/agent/spec-manifest.json +2 -2
- package/runtime/prime/AGENTS.md +62 -58
- package/runtime/prime/bin/activity-action.sh +92 -0
- package/runtime/prime/bin/activity-data.sh +12 -233
- package/runtime/prime/bin/agent.sh +84 -100
- package/runtime/prime/bin/artifact-register.sh +164 -0
- package/runtime/prime/bin/artifact-writeback.sh +53 -0
- package/runtime/prime/bin/artifacts.sh +288 -0
- package/runtime/prime/bin/build-sealed-dataroom.mjs +722 -0
- package/runtime/prime/bin/capture-run.sh +18 -2
- package/runtime/prime/bin/codex-render-spec.mjs +41 -16
- package/runtime/prime/bin/codex-session-start.sh +14 -4
- package/runtime/prime/bin/codex-skill-render.mjs +9 -7
- package/runtime/prime/bin/corpus_map.py +806 -0
- package/runtime/prime/bin/corpus_survey.py +304 -0
- package/runtime/prime/bin/dashboard-data.sh +11 -171
- package/runtime/prime/bin/github-auth.sh +121 -0
- package/runtime/prime/bin/graph-batch.sh +7 -0
- package/runtime/prime/bin/graph-op.sh +4 -0
- package/runtime/prime/bin/graph-projection.sh +54 -0
- package/runtime/prime/bin/graph-wal.sh +13 -0
- package/runtime/prime/bin/graph.sh +44 -0
- package/runtime/prime/bin/handoff-lifecycle-job.sh +146 -0
- package/runtime/prime/bin/handoff-lifecycle.sh +280 -0
- package/runtime/prime/bin/handoff-preview.sh +169 -0
- package/runtime/prime/bin/handoff-run.sh +42 -26
- package/runtime/prime/bin/ingest-graph.sh +6 -0
- package/runtime/prime/bin/ingest-item.sh +10 -0
- package/runtime/prime/bin/ingest.sh +59 -30
- package/runtime/prime/bin/invite.sh +6 -0
- package/runtime/prime/bin/issue.sh +10 -0
- package/runtime/prime/bin/knowledge.sh +10 -0
- package/runtime/prime/bin/lib/config.sh +20 -0
- package/runtime/prime/bin/lib/context.sh +46 -25
- package/runtime/prime/bin/lib/git-sync.sh +70 -6
- package/runtime/prime/bin/lib/greeting.sh +34 -5
- package/runtime/prime/bin/lib/handoff-meta.sh +97 -0
- package/runtime/prime/bin/lib/worktree-links.sh +6 -0
- package/runtime/prime/bin/node-run.sh +42 -0
- package/runtime/prime/bin/notification.sh +6 -0
- package/runtime/prime/bin/observe-context.sh +14 -0
- package/runtime/prime/bin/onboarding.sh +6 -0
- package/runtime/prime/bin/person.py +4 -0
- package/runtime/prime/bin/person.sh +43 -10
- package/runtime/prime/bin/pi-observe-context.mjs +93 -0
- package/runtime/prime/bin/pi-product-workflows.mjs +1 -1
- package/runtime/prime/bin/pi-render-spec.mjs +140 -0
- package/runtime/prime/bin/post-update.sh +37 -0
- package/runtime/prime/bin/prime-render-spec.mjs +1 -1
- package/runtime/prime/bin/publish-artifact.sh +55 -2
- package/runtime/prime/bin/quest.sh +9 -0
- package/runtime/prime/bin/question.sh +8 -0
- package/runtime/prime/bin/research-ingest.sh +7 -0
- package/runtime/prime/bin/restore-owned-skills.sh +67 -0
- package/runtime/prime/bin/retrieval_eval.py +385 -0
- package/runtime/prime/bin/runtime-identity.sh +70 -0
- package/runtime/prime/bin/runtime-upgrade.sh +43 -0
- package/runtime/prime/bin/scroll.sh +9 -0
- package/runtime/prime/bin/search.sh +231 -195
- package/runtime/prime/bin/settings.sh +174 -5
- package/runtime/prime/bin/startup-check.sh +136 -0
- package/runtime/prime/bin/telemetry.sh +522 -247
- package/runtime/prime/bin/test-changes.sh +344 -0
- package/runtime/prime/bin/thread.sh +9 -0
- package/runtime/prime/bin/todo.sh +9 -0
- package/runtime/prime/bin/view-render.sh +126 -0
- package/runtime/prime/bin/worktree-create.sh +19 -5
- package/runtime/prime/egregore_runtime/__init__.py +228 -0
- package/runtime/prime/egregore_runtime/adapters/__init__.py +5 -0
- package/runtime/prime/egregore_runtime/adapters/invitation.py +237 -0
- package/runtime/prime/egregore_runtime/adapters/notification.py +59 -0
- package/runtime/prime/egregore_runtime/adapters/qmd.py +1701 -0
- package/runtime/prime/egregore_runtime/adapters/qmd_cli.py +210 -0
- package/runtime/prime/egregore_runtime/adapters/qmd_embed_worker.py +59 -0
- package/runtime/prime/egregore_runtime/admin_gate.py +102 -0
- package/runtime/prime/egregore_runtime/artifacts.py +523 -0
- package/runtime/prime/egregore_runtime/contracts.py +499 -0
- package/runtime/prime/egregore_runtime/entitlements.py +85 -0
- package/runtime/prime/egregore_runtime/errors.py +10 -0
- package/runtime/prime/egregore_runtime/graph.py +356 -0
- package/runtime/prime/egregore_runtime/handoff_lookup.py +196 -0
- package/runtime/prime/egregore_runtime/harness_cli.py +1221 -0
- package/runtime/prime/egregore_runtime/identity.py +734 -0
- package/runtime/prime/egregore_runtime/identity_cli.py +91 -0
- package/runtime/prime/egregore_runtime/ingest.py +575 -0
- package/runtime/prime/egregore_runtime/ingest_cli.py +254 -0
- package/runtime/prime/egregore_runtime/ingest_index_cli.py +147 -0
- package/runtime/prime/egregore_runtime/invitation_cli.py +51 -0
- package/runtime/prime/egregore_runtime/invitations.py +116 -0
- package/runtime/prime/egregore_runtime/issue_cli.py +305 -0
- package/runtime/prime/egregore_runtime/issues.py +359 -0
- package/runtime/prime/egregore_runtime/knowledge.py +376 -0
- package/runtime/prime/egregore_runtime/knowledge_cli.py +173 -0
- package/runtime/prime/egregore_runtime/lifecycle.py +888 -0
- package/runtime/prime/egregore_runtime/lifecycle_cli.py +269 -0
- package/runtime/prime/egregore_runtime/notification_cli.py +125 -0
- package/runtime/prime/egregore_runtime/notifications.py +230 -0
- package/runtime/prime/egregore_runtime/observe.py +342 -0
- package/runtime/prime/egregore_runtime/onboarding.py +290 -0
- package/runtime/prime/egregore_runtime/onboarding_cli.py +110 -0
- package/runtime/prime/egregore_runtime/policy.py +144 -0
- package/runtime/prime/egregore_runtime/profile.py +193 -0
- package/runtime/prime/egregore_runtime/quest_cli.py +216 -0
- package/runtime/prime/egregore_runtime/question_cli.py +180 -0
- package/runtime/prime/egregore_runtime/questions.py +304 -0
- package/runtime/prime/egregore_runtime/quests.py +419 -0
- package/runtime/prime/egregore_runtime/readiness.py +379 -0
- package/runtime/prime/egregore_runtime/research_ingest.py +256 -0
- package/runtime/prime/egregore_runtime/research_ingest_cli.py +44 -0
- package/runtime/prime/egregore_runtime/runtime.py +464 -0
- package/runtime/prime/egregore_runtime/scroll_cli.py +185 -0
- package/runtime/prime/egregore_runtime/scrolls.py +726 -0
- package/runtime/prime/egregore_runtime/status.py +412 -0
- package/runtime/prime/egregore_runtime/status_cli.py +174 -0
- package/runtime/prime/egregore_runtime/sync.py +218 -0
- package/runtime/prime/egregore_runtime/teamsync.py +139 -0
- package/runtime/prime/egregore_runtime/telemetry.py +640 -0
- package/runtime/prime/egregore_runtime/thread_cli.py +173 -0
- package/runtime/prime/egregore_runtime/threads.py +517 -0
- package/runtime/prime/egregore_runtime/todo_cli.py +142 -0
- package/runtime/prime/egregore_runtime/todos.py +244 -0
- package/runtime/prime/egregore_runtime/upgrade.py +770 -0
- package/runtime/prime/egregore_runtime/upgrade_worker.py +53 -0
- package/runtime/prime/egregore_runtime/writeback.py +391 -0
- package/runtime/prime/egregore_runtime/writeback_cli.py +114 -0
- package/runtime/prime/manifest.json +98 -1
|
@@ -0,0 +1,1011 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Deterministic, org-scoped intake substrate for Egregore.
|
|
3
|
+
|
|
4
|
+
The agent skill owns interpretation and curation. This program owns the parts
|
|
5
|
+
that must not depend on model judgment: stable identities, provenance,
|
|
6
|
+
deterministic chunks, boundary metadata, manifests, and retrieval filtering.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import hashlib
|
|
13
|
+
import html
|
|
14
|
+
import json
|
|
15
|
+
import mimetypes
|
|
16
|
+
import os
|
|
17
|
+
import re
|
|
18
|
+
import shutil
|
|
19
|
+
import subprocess
|
|
20
|
+
import sys
|
|
21
|
+
import zipfile
|
|
22
|
+
from datetime import datetime, timezone
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from xml.etree import ElementTree
|
|
25
|
+
|
|
26
|
+
sys.path.append(str(Path(__file__).resolve().parent))
|
|
27
|
+
import catalogue as catalogue_module # noqa: E402
|
|
28
|
+
import doc_segment # noqa: E402 (same-directory module, path set above)
|
|
29
|
+
import pdf_extract # noqa: E402
|
|
30
|
+
import source_profile # noqa: E402
|
|
31
|
+
import text_repair # noqa: E402
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
35
|
+
CONFIG = ROOT / "egregore.json"
|
|
36
|
+
TEXT_EXTS = {".txt", ".md", ".markdown", ".rst", ".csv", ".tsv", ".json", ".jsonl", ".yaml", ".yml", ".xml"}
|
|
37
|
+
HTML_EXTS = {".html", ".htm"}
|
|
38
|
+
SELECTION_SCHEMA = "egregore-ingest-selection/v1"
|
|
39
|
+
EXTRACTION_SCHEMA = "egregore-ingest-extraction/v1"
|
|
40
|
+
CORPUS_SCHEMA = "egregore-ingest-corpus/v1"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def load_config() -> dict:
|
|
44
|
+
with CONFIG.open(encoding="utf-8") as handle:
|
|
45
|
+
return json.load(handle)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def metadata_root() -> Path:
|
|
49
|
+
override = os.environ.get("EGREGORE_INGEST_META_ROOT")
|
|
50
|
+
return Path(override).resolve() if override else (ROOT / "memory" / "ingest").resolve()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def data_root() -> Path:
|
|
54
|
+
override = os.environ.get("EGREGORE_INGEST_ROOT")
|
|
55
|
+
return Path(override).resolve() if override else (ROOT / ".egregore" / "ingest").resolve()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def staging_root() -> Path:
|
|
59
|
+
override = os.environ.get("EGREGORE_INGEST_STAGING_ROOT")
|
|
60
|
+
return Path(override).resolve() if override else (ROOT / ".egregore" / "ingest-staging").resolve()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def producer_session_id() -> str:
|
|
64
|
+
trusted = os.environ.get("EGREGORE_SESSION_ID", "").strip()
|
|
65
|
+
if trusted:
|
|
66
|
+
return trusted
|
|
67
|
+
session_file = ROOT / ".egregore-session-id"
|
|
68
|
+
if session_file.is_file():
|
|
69
|
+
return session_file.read_text(encoding="utf-8").strip()
|
|
70
|
+
return ""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def slug(value: str) -> str:
|
|
74
|
+
result = re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-")
|
|
75
|
+
if not result:
|
|
76
|
+
raise ValueError("value must contain at least one letter or number")
|
|
77
|
+
return result
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def digest(value: bytes | str, length: int = 24) -> str:
|
|
81
|
+
raw = value.encode("utf-8") if isinstance(value, str) else value
|
|
82
|
+
return hashlib.sha256(raw).hexdigest()[:length]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def hash_file(path: Path) -> tuple[str, int]:
|
|
86
|
+
hasher = hashlib.sha256()
|
|
87
|
+
size = 0
|
|
88
|
+
with path.open("rb") as handle:
|
|
89
|
+
while block := handle.read(1024 * 1024):
|
|
90
|
+
hasher.update(block)
|
|
91
|
+
size += len(block)
|
|
92
|
+
return hasher.hexdigest(), size
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def parse_boundaries(items: list[str]) -> dict[str, str]:
|
|
96
|
+
result: dict[str, str] = {}
|
|
97
|
+
for item in items:
|
|
98
|
+
if "=" not in item:
|
|
99
|
+
raise ValueError(f"boundary must be key=value: {item}")
|
|
100
|
+
key, value = item.split("=", 1)
|
|
101
|
+
key = slug(key)
|
|
102
|
+
value = value.strip()
|
|
103
|
+
if not value:
|
|
104
|
+
raise ValueError(f"boundary value is empty: {item}")
|
|
105
|
+
result[key] = value
|
|
106
|
+
return dict(sorted(result.items()))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def read_text(path: Path) -> tuple[str | None, str | None, str | None, dict | None]:
|
|
110
|
+
"""Extract text. Returns (text, error, extractor, detail).
|
|
111
|
+
|
|
112
|
+
`detail` carries per-page extraction facts for formats that have pages, so the
|
|
113
|
+
manifest records which pages needed OCR and which remain sparse or blank.
|
|
114
|
+
"""
|
|
115
|
+
ext = path.suffix.lower()
|
|
116
|
+
try:
|
|
117
|
+
if ext in TEXT_EXTS:
|
|
118
|
+
return path.read_text(encoding="utf-8", errors="replace"), None, "builtin-text", None
|
|
119
|
+
if ext in HTML_EXTS:
|
|
120
|
+
raw = path.read_text(encoding="utf-8", errors="replace")
|
|
121
|
+
raw = re.sub(r"(?is)<(script|style).*?>.*?</\1>", " ", raw)
|
|
122
|
+
return html.unescape(re.sub(r"(?s)<[^>]+>", " ", raw)), None, "builtin-html", None
|
|
123
|
+
if ext == ".docx":
|
|
124
|
+
with zipfile.ZipFile(path) as archive:
|
|
125
|
+
raw = archive.read("word/document.xml")
|
|
126
|
+
root = ElementTree.fromstring(raw)
|
|
127
|
+
text = "\n".join(t.text or "" for t in root.iter() if t.tag.endswith("}t"))
|
|
128
|
+
return text, None, "builtin-docx", None
|
|
129
|
+
if ext == ".pdf":
|
|
130
|
+
result = pdf_extract.extract(path)
|
|
131
|
+
return result.text, result.error, result.extractor, (result.report or None)
|
|
132
|
+
except (OSError, ValueError, zipfile.BadZipFile, ElementTree.ParseError) as exc:
|
|
133
|
+
return None, str(exc), None, None
|
|
134
|
+
return None, f"unsupported extension {ext or '(none)'}", None, None
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def chunks(text: str, limit: int = 7000) -> list[str]:
|
|
138
|
+
paragraphs = [p.strip() for p in re.split(r"\n\s*\n", text) if p.strip()]
|
|
139
|
+
if not paragraphs and text.strip():
|
|
140
|
+
paragraphs = [text.strip()]
|
|
141
|
+
result: list[str] = []
|
|
142
|
+
current = ""
|
|
143
|
+
for paragraph in paragraphs:
|
|
144
|
+
pieces = [paragraph[i : i + limit] for i in range(0, len(paragraph), limit)] or [""]
|
|
145
|
+
for piece in pieces:
|
|
146
|
+
candidate = f"{current}\n\n{piece}".strip() if current else piece
|
|
147
|
+
if current and len(candidate) > limit:
|
|
148
|
+
result.append(current)
|
|
149
|
+
current = piece
|
|
150
|
+
else:
|
|
151
|
+
current = candidate
|
|
152
|
+
if current:
|
|
153
|
+
result.append(current)
|
|
154
|
+
return result
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# Catalogue columns that carry provenance, by what they mean rather than by
|
|
158
|
+
# what any one organisation calls them. A profile's `map` already renames its
|
|
159
|
+
# own columns to these, so no further configuration is needed.
|
|
160
|
+
CATALOGUE_URL_FIELDS = ("url", "source_url", "link")
|
|
161
|
+
CATALOGUE_INSTITUTION_FIELDS = ("institution", "publisher", "source_type", "source")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def catalogue_provenance(catalogued: dict) -> tuple:
|
|
165
|
+
"""The URL and institution a catalogue records for a document.
|
|
166
|
+
|
|
167
|
+
The resolver has always accepted these and ranks both above a filename,
|
|
168
|
+
because a filename is what someone happened to save a file as. Ingest
|
|
169
|
+
passed only the filename, so the stronger evidence sitting in the row it
|
|
170
|
+
had just read went unused.
|
|
171
|
+
|
|
172
|
+
Measured on a real archive that was not a marginal loss. Seventy articles
|
|
173
|
+
downloaded from two journal indexes were saved under bare article titles,
|
|
174
|
+
carrying no publisher token to match. The profile had rules for both
|
|
175
|
+
indexes and the catalogue named the index for every one of them, and still
|
|
176
|
+
nothing matched — the one input that could have identified them was the one
|
|
177
|
+
input never passed.
|
|
178
|
+
"""
|
|
179
|
+
if not catalogued:
|
|
180
|
+
return "", ""
|
|
181
|
+
url = next((str(catalogued[f]) for f in CATALOGUE_URL_FIELDS if catalogued.get(f)), "")
|
|
182
|
+
institution = next(
|
|
183
|
+
(str(catalogued[f]) for f in CATALOGUE_INSTITUTION_FIELDS if catalogued.get(f)), "")
|
|
184
|
+
return url, institution
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def find_publisher_profile() -> Path | None:
|
|
188
|
+
"""Where this instance's publisher profile is, if it has one.
|
|
189
|
+
|
|
190
|
+
The profile is what assigns a zone and a trust tier to each document, and
|
|
191
|
+
zone is a hard retrieval boundary. Ingesting without it produces a corpus
|
|
192
|
+
where every document is unplaced, so every later question is refused — a
|
|
193
|
+
failure that looks like an empty archive rather than a missing setting.
|
|
194
|
+
|
|
195
|
+
So this looks in the places a profile actually lives rather than in one
|
|
196
|
+
fixed path. An instance keeps its ontology under memory/knowledge/tools/,
|
|
197
|
+
not beside its intake metadata, and requiring an environment variable to
|
|
198
|
+
bridge that gap means the common case is the broken one.
|
|
199
|
+
"""
|
|
200
|
+
override = os.environ.get("EGREGORE_PUBLISHER_PROFILE")
|
|
201
|
+
if override:
|
|
202
|
+
path = Path(override)
|
|
203
|
+
return path if path.is_file() else None
|
|
204
|
+
|
|
205
|
+
candidates = [metadata_root() / "publisher-profile.json"]
|
|
206
|
+
memory = Path("memory")
|
|
207
|
+
if memory.is_dir():
|
|
208
|
+
candidates.extend(sorted(memory.glob("knowledge/tools/*/publisher-profile.json")))
|
|
209
|
+
candidates.append(memory / "publisher-profile.json")
|
|
210
|
+
for path in candidates:
|
|
211
|
+
if path.is_file():
|
|
212
|
+
return path
|
|
213
|
+
return None
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def publisher_profile() -> dict | None:
|
|
217
|
+
"""Load the instance's publisher profile, if one is configured.
|
|
218
|
+
|
|
219
|
+
Resolution is recorded as evidence only. It never overrides a boundary the
|
|
220
|
+
operator set: deriving a region is an inference, and an inference must not
|
|
221
|
+
silently replace a stated fact.
|
|
222
|
+
"""
|
|
223
|
+
path = find_publisher_profile()
|
|
224
|
+
if path is None:
|
|
225
|
+
return None
|
|
226
|
+
try:
|
|
227
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
228
|
+
except (OSError, json.JSONDecodeError):
|
|
229
|
+
return None
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def source_dir(source_id: str) -> Path:
|
|
233
|
+
return metadata_root() / "sources" / slug(source_id)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def load_manifest(directory: Path) -> dict:
|
|
237
|
+
path = directory / "manifest.json"
|
|
238
|
+
if not path.exists():
|
|
239
|
+
return {"documents": []}
|
|
240
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def atomic_json(path: Path, value: object) -> None:
|
|
244
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
245
|
+
temporary = path.with_suffix(path.suffix + ".tmp")
|
|
246
|
+
temporary.write_text(json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
247
|
+
temporary.replace(path)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def atomic_text(path: Path, value: str) -> None:
|
|
251
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
252
|
+
temporary = path.with_suffix(path.suffix + ".tmp")
|
|
253
|
+
temporary.write_text(value, encoding="utf-8")
|
|
254
|
+
temporary.replace(path)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def candidate_files(path: Path) -> tuple[Path, list[Path]]:
|
|
258
|
+
resolved = path.resolve()
|
|
259
|
+
if resolved.is_file():
|
|
260
|
+
return resolved.parent, [resolved]
|
|
261
|
+
if not resolved.is_dir():
|
|
262
|
+
raise ValueError(f"source path does not exist: {path}")
|
|
263
|
+
files = [
|
|
264
|
+
p
|
|
265
|
+
for p in resolved.rglob("*")
|
|
266
|
+
if p.is_file()
|
|
267
|
+
and not p.is_symlink()
|
|
268
|
+
and not any(part.startswith(".") for part in p.relative_to(resolved).parts)
|
|
269
|
+
]
|
|
270
|
+
return resolved, sorted(files, key=lambda p: p.as_posix())
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def add_path(args: argparse.Namespace) -> dict:
|
|
274
|
+
config = load_config()
|
|
275
|
+
org = slug(config.get("slug") or "egregore")
|
|
276
|
+
source_id = slug(args.source)
|
|
277
|
+
boundaries = parse_boundaries(args.boundary)
|
|
278
|
+
input_path = Path(args.path).resolve()
|
|
279
|
+
base, files = candidate_files(input_path)
|
|
280
|
+
directory = source_dir(source_id)
|
|
281
|
+
docs_dir = data_root() / "sources" / source_id / "documents"
|
|
282
|
+
docs_dir.mkdir(parents=True, exist_ok=True)
|
|
283
|
+
existing_manifest = load_manifest(directory)
|
|
284
|
+
previous = {d["id"]: d for d in existing_manifest.get("documents", [])}
|
|
285
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
286
|
+
profile_path = find_publisher_profile()
|
|
287
|
+
profile = publisher_profile()
|
|
288
|
+
if profile is None:
|
|
289
|
+
# Said once, plainly, because the alternative is a corpus that ingests
|
|
290
|
+
# cleanly and then refuses every question with no indication why.
|
|
291
|
+
print("ingest: no publisher-profile.json found — documents will carry no zone "
|
|
292
|
+
"or trust tier, and a zoneless document can never be served. Put one "
|
|
293
|
+
"under memory/knowledge/tools/<ontology>/ or set "
|
|
294
|
+
"EGREGORE_PUBLISHER_PROFILE.", file=sys.stderr)
|
|
295
|
+
catalogue = catalogue_module.load(
|
|
296
|
+
profile or {}, base=profile_path.parent if profile_path else None
|
|
297
|
+
)
|
|
298
|
+
repair_map = text_repair.load_mapping(profile or {})
|
|
299
|
+
ingested = unchanged = metadata_updated = skipped = pruned = 0
|
|
300
|
+
# How often progress is written. The manifest used to be saved once, after
|
|
301
|
+
# every file, so an interrupted run lost everything it had extracted — on a
|
|
302
|
+
# 921-document folder that was two and a half hours of OCR with nothing to
|
|
303
|
+
# show. Documents already on disk were re-extracted because nothing recorded
|
|
304
|
+
# them. Writing periodically means an interruption costs at most this many.
|
|
305
|
+
checkpoint_every = int(os.environ.get("EGREGORE_INGEST_CHECKPOINT", "25") or 0)
|
|
306
|
+
since_checkpoint = 0
|
|
307
|
+
# Read before the loop rather than after it, so a checkpoint writes the real
|
|
308
|
+
# deletion records instead of dropping them. Pruning still happens later; a
|
|
309
|
+
# mid-run save simply carries forward what the previous run recorded.
|
|
310
|
+
tombstones = existing_manifest.get("tombstones", [])
|
|
311
|
+
errors: list[dict] = []
|
|
312
|
+
seen_paths: set[str] = set()
|
|
313
|
+
|
|
314
|
+
for path in files:
|
|
315
|
+
relative = path.relative_to(base).as_posix() if path != base else path.name
|
|
316
|
+
seen_paths.add(relative)
|
|
317
|
+
doc_id = digest(f"{org}\0{source_id}\0{relative}")
|
|
318
|
+
content_hash, size_bytes = hash_file(path)
|
|
319
|
+
existing = previous.get(doc_id)
|
|
320
|
+
registered = getattr(args, "extractions", {}).get(relative)
|
|
321
|
+
if (
|
|
322
|
+
existing
|
|
323
|
+
and existing.get("content_hash") == content_hash
|
|
324
|
+
and existing.get("boundaries", {}) == boundaries
|
|
325
|
+
and not registered
|
|
326
|
+
and existing.get("status") == "unverified"
|
|
327
|
+
):
|
|
328
|
+
unchanged += 1
|
|
329
|
+
continue
|
|
330
|
+
if registered:
|
|
331
|
+
text, error = registered["text"], None
|
|
332
|
+
extraction = {"kind": "runtime", "extractor": registered["extractor"]}
|
|
333
|
+
else:
|
|
334
|
+
text, error, extractor, detail = read_text(path)
|
|
335
|
+
extraction = {"kind": "local", "extractor": extractor} if extractor else None
|
|
336
|
+
if extraction and detail:
|
|
337
|
+
extraction["pages"] = detail
|
|
338
|
+
# Fix characters the document's own font encoding got wrong, before
|
|
339
|
+
# anything downstream indexes or quotes them.
|
|
340
|
+
repaired = None
|
|
341
|
+
if text and repair_map:
|
|
342
|
+
outcome = text_repair.repair(text, repair_map)
|
|
343
|
+
if outcome.replaced:
|
|
344
|
+
text = outcome.text
|
|
345
|
+
repaired = outcome.as_report()
|
|
346
|
+
|
|
347
|
+
if error or text is None or not text.strip():
|
|
348
|
+
skipped += 1
|
|
349
|
+
errors.append({"path": relative, "error": error or "empty extracted text"})
|
|
350
|
+
if existing:
|
|
351
|
+
normalized = data_root() / existing["file_path"]
|
|
352
|
+
normalized.unlink(missing_ok=True)
|
|
353
|
+
previous[doc_id] = {
|
|
354
|
+
**existing,
|
|
355
|
+
"content_hash": content_hash,
|
|
356
|
+
"revision": content_hash[:16],
|
|
357
|
+
"mime": mimetypes.guess_type(path.name)[0] or "application/octet-stream",
|
|
358
|
+
"bytes": size_bytes,
|
|
359
|
+
"chunks": [],
|
|
360
|
+
"extraction": None,
|
|
361
|
+
"boundaries": boundaries,
|
|
362
|
+
"status": "extraction_failed",
|
|
363
|
+
"ingested_at": now,
|
|
364
|
+
}
|
|
365
|
+
continue
|
|
366
|
+
normalized_hash = hashlib.sha256(text.encode()).hexdigest()
|
|
367
|
+
if (
|
|
368
|
+
existing
|
|
369
|
+
and existing.get("content_hash") == content_hash
|
|
370
|
+
and existing.get("normalized_content_hash") == normalized_hash
|
|
371
|
+
and existing.get("boundaries", {}) == boundaries
|
|
372
|
+
and existing.get("status") == "unverified"
|
|
373
|
+
):
|
|
374
|
+
unchanged += 1
|
|
375
|
+
continue
|
|
376
|
+
# Segment before chunking so no passage straddles two works. A document
|
|
377
|
+
# holding a single work yields one segment, and chunking is unchanged.
|
|
378
|
+
if repaired and extraction:
|
|
379
|
+
extraction["repair"] = repaired
|
|
380
|
+
|
|
381
|
+
segments = doc_segment.segment(text)
|
|
382
|
+
document_chunks: list[tuple[str, doc_segment.Segment]] = []
|
|
383
|
+
for seg in segments:
|
|
384
|
+
for piece in chunks(seg.text(text)):
|
|
385
|
+
document_chunks.append((piece, seg))
|
|
386
|
+
|
|
387
|
+
chunk_rows = []
|
|
388
|
+
body = []
|
|
389
|
+
for index, (content, seg) in enumerate(document_chunks, 1):
|
|
390
|
+
chunk_id = f"{doc_id}-c{index:04d}-{digest(content, 8)}"
|
|
391
|
+
row = {"id": chunk_id, "index": index, "sha256": hashlib.sha256(content.encode()).hexdigest()}
|
|
392
|
+
if seg.kind != "article" or seg.label:
|
|
393
|
+
row["segment"] = {"kind": seg.kind, **({"label": seg.label} if seg.label else {})}
|
|
394
|
+
chunk_rows.append(row)
|
|
395
|
+
body.append(f"## Chunk {index:04d}\n\n<!-- ingest-chunk: {chunk_id} -->\n\n{content}")
|
|
396
|
+
|
|
397
|
+
structure = {
|
|
398
|
+
"segments": len([s for s in segments if s.kind == "article"]),
|
|
399
|
+
"summary": doc_segment.summarise(segments),
|
|
400
|
+
}
|
|
401
|
+
derived_year = doc_segment.publication_year(segments)
|
|
402
|
+
if derived_year:
|
|
403
|
+
structure["publication_year"] = derived_year
|
|
404
|
+
structure["year_source"] = "running_head"
|
|
405
|
+
|
|
406
|
+
# Fill in boundaries the operator left blank, when the instance supplies a
|
|
407
|
+
# profile that can work them out from where the document came from. People
|
|
408
|
+
# dumping a folder of documents will not type these in, and a boundary that
|
|
409
|
+
# is absent filters nothing. A derived value is marked as derived: anything
|
|
410
|
+
# downstream can tell it apart from one a person stated, and weigh it by the
|
|
411
|
+
# confidence recorded alongside. An operator's value is never overwritten.
|
|
412
|
+
# Evidence strongest first: what the operator stated, then what the
|
|
413
|
+
# organisation's own catalogue records, then what can be inferred from
|
|
414
|
+
# the document's origin. A weaker source never overwrites a stronger one.
|
|
415
|
+
provenance = None
|
|
416
|
+
doc_boundaries = dict(boundaries)
|
|
417
|
+
catalogued = catalogue_module.lookup(catalogue, relative)
|
|
418
|
+
|
|
419
|
+
if catalogued:
|
|
420
|
+
provenance = {"from_catalogue": {k: v for k, v in catalogued.items()}}
|
|
421
|
+
for field_name, value in catalogued.items():
|
|
422
|
+
if field_name in ("zone", "crop", "topic", "customer", "jurisdiction", "project"):
|
|
423
|
+
if field_name not in doc_boundaries:
|
|
424
|
+
doc_boundaries[field_name] = value
|
|
425
|
+
provenance["applied_boundary"] = field_name
|
|
426
|
+
if catalogued.get("year"):
|
|
427
|
+
structure.setdefault("publication_year", catalogued["year"])
|
|
428
|
+
structure["year_source"] = structure.get("year_source") or "catalogue"
|
|
429
|
+
if catalogued.get("tier"):
|
|
430
|
+
provenance["tier"] = catalogued["tier"]
|
|
431
|
+
provenance["confidence"] = "high"
|
|
432
|
+
|
|
433
|
+
if profile:
|
|
434
|
+
catalogue_url, catalogue_institution = catalogue_provenance(catalogued)
|
|
435
|
+
resolved = source_profile.resolve(
|
|
436
|
+
profile, url=catalogue_url, institution=catalogue_institution,
|
|
437
|
+
filename=relative)
|
|
438
|
+
if resolved.publisher_class or resolved.notes:
|
|
439
|
+
inferred = resolved.as_dict()
|
|
440
|
+
# Keep the inference visible even when the catalogue answered,
|
|
441
|
+
# so a disagreement between the two is inspectable.
|
|
442
|
+
provenance = {**inferred, **(provenance or {})} if provenance else inferred
|
|
443
|
+
key = source_profile.boundary_key(profile)
|
|
444
|
+
if key and resolved.zone and key not in doc_boundaries:
|
|
445
|
+
doc_boundaries[key] = resolved.zone
|
|
446
|
+
provenance = {**(provenance or {}), "applied_boundary": key, "derived": True}
|
|
447
|
+
out_path = docs_dir / f"{doc_id}.md"
|
|
448
|
+
frontmatter = {
|
|
449
|
+
"id": doc_id,
|
|
450
|
+
"type": "ingest-document",
|
|
451
|
+
"org": org,
|
|
452
|
+
"source_id": source_id,
|
|
453
|
+
"source_path": relative,
|
|
454
|
+
"title": path.stem,
|
|
455
|
+
"content_hash": content_hash,
|
|
456
|
+
"normalized_content_hash": normalized_hash,
|
|
457
|
+
"revision": content_hash[:16],
|
|
458
|
+
"status": "unverified",
|
|
459
|
+
"boundaries": doc_boundaries,
|
|
460
|
+
"extraction": extraction,
|
|
461
|
+
"structure": structure,
|
|
462
|
+
"provenance": provenance,
|
|
463
|
+
"ingested_at": now,
|
|
464
|
+
}
|
|
465
|
+
yaml_lines = ["---"] + [f"{key}: {json.dumps(value, ensure_ascii=False)}" for key, value in frontmatter.items()] + ["---", ""]
|
|
466
|
+
atomic_text(out_path, "\n".join(yaml_lines) + f"# {path.stem}\n\n" + "\n\n".join(body) + "\n")
|
|
467
|
+
previous[doc_id] = {
|
|
468
|
+
"id": doc_id,
|
|
469
|
+
"title": path.stem,
|
|
470
|
+
"source_path": relative,
|
|
471
|
+
"file_path": out_path.relative_to(data_root()).as_posix(),
|
|
472
|
+
"content_hash": content_hash,
|
|
473
|
+
"normalized_content_hash": normalized_hash,
|
|
474
|
+
"revision": content_hash[:16],
|
|
475
|
+
"mime": mimetypes.guess_type(path.name)[0] or "application/octet-stream",
|
|
476
|
+
"bytes": size_bytes,
|
|
477
|
+
"chunks": chunk_rows,
|
|
478
|
+
"extraction": extraction,
|
|
479
|
+
"structure": structure,
|
|
480
|
+
"provenance": provenance,
|
|
481
|
+
"boundaries": doc_boundaries,
|
|
482
|
+
"status": "unverified",
|
|
483
|
+
"ingested_at": now,
|
|
484
|
+
}
|
|
485
|
+
since_checkpoint += 1
|
|
486
|
+
if checkpoint_every and since_checkpoint >= checkpoint_every:
|
|
487
|
+
# A crash between the document write and this one costs re-extraction
|
|
488
|
+
# of at most `checkpoint_every` files, never the whole run.
|
|
489
|
+
write_manifest(directory, source_id, org, args, boundaries, base, now,
|
|
490
|
+
previous, tombstones, errors)
|
|
491
|
+
since_checkpoint = 0
|
|
492
|
+
if existing and existing.get("content_hash") == content_hash:
|
|
493
|
+
metadata_updated += 1
|
|
494
|
+
else:
|
|
495
|
+
ingested += 1
|
|
496
|
+
|
|
497
|
+
if args.prune:
|
|
498
|
+
if not input_path.is_dir():
|
|
499
|
+
raise ValueError("--prune requires a directory snapshot")
|
|
500
|
+
for doc_id, document in list(previous.items()):
|
|
501
|
+
if document["source_path"] in seen_paths:
|
|
502
|
+
continue
|
|
503
|
+
normalized = data_root() / document["file_path"]
|
|
504
|
+
if normalized.is_file():
|
|
505
|
+
normalized.unlink()
|
|
506
|
+
tombstones.append({
|
|
507
|
+
"id": doc_id,
|
|
508
|
+
"source_path": document["source_path"],
|
|
509
|
+
"content_hash": document["content_hash"],
|
|
510
|
+
"deleted_at": now,
|
|
511
|
+
})
|
|
512
|
+
del previous[doc_id]
|
|
513
|
+
pruned += 1
|
|
514
|
+
# A path can disappear in one authoritative snapshot and return later.
|
|
515
|
+
# Current documents always win over historical deletion records.
|
|
516
|
+
tombstones = [row for row in tombstones if row["id"] not in previous]
|
|
517
|
+
|
|
518
|
+
write_manifest(directory, source_id, org, args, boundaries, base, now,
|
|
519
|
+
previous, tombstones, errors)
|
|
520
|
+
return {"org": org, "source": source_id, "ingested": ingested, "unchanged": unchanged, "metadata_updated": metadata_updated, "skipped": skipped, "pruned": pruned, "documents": len(previous), "metadata_root": str(directory), "data_root": str(docs_dir)}
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def write_manifest(directory, source_id, org, args, boundaries, base, now,
|
|
524
|
+
previous, tombstones, errors) -> None:
|
|
525
|
+
"""Persist progress. Called periodically during a run and once at the end.
|
|
526
|
+
|
|
527
|
+
One writer for both so a checkpoint and a completed run cannot record the
|
|
528
|
+
corpus differently.
|
|
529
|
+
"""
|
|
530
|
+
source_record = {
|
|
531
|
+
"id": source_id,
|
|
532
|
+
"org": org,
|
|
533
|
+
"name": getattr(args, "name", None) or source_id,
|
|
534
|
+
"kind": getattr(args, "kind", None),
|
|
535
|
+
"boundaries": boundaries,
|
|
536
|
+
"root_hint": base.name,
|
|
537
|
+
"updated_at": now,
|
|
538
|
+
}
|
|
539
|
+
atomic_json(directory / "source.json", source_record)
|
|
540
|
+
atomic_json(
|
|
541
|
+
directory / "manifest.json",
|
|
542
|
+
{
|
|
543
|
+
"schema": CORPUS_SCHEMA,
|
|
544
|
+
"producer_session": producer_session_id(),
|
|
545
|
+
"source": source_record,
|
|
546
|
+
"documents": sorted(previous.values(), key=lambda d: d["id"]),
|
|
547
|
+
"tombstones": tombstones,
|
|
548
|
+
"errors": errors,
|
|
549
|
+
},
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def cmd_add(args: argparse.Namespace) -> int:
|
|
554
|
+
print(json.dumps(add_path(args), indent=2))
|
|
555
|
+
return 0
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def rewrite_frontmatter(path: Path, updates: dict) -> bool:
|
|
559
|
+
"""Replace named frontmatter fields in place, leaving the text untouched.
|
|
560
|
+
|
|
561
|
+
A document's placement lives in two files — the manifest record and this
|
|
562
|
+
document's own frontmatter — and a reader may consult either. Updating one
|
|
563
|
+
and not the other produces a corpus that disagrees with itself, which is
|
|
564
|
+
worse than the stale value it replaced.
|
|
565
|
+
|
|
566
|
+
Only the named lines are rewritten. The body is never reparsed, so nothing
|
|
567
|
+
that survived extraction can be lost by a maintenance pass.
|
|
568
|
+
"""
|
|
569
|
+
try:
|
|
570
|
+
text = path.read_text(encoding="utf-8")
|
|
571
|
+
except OSError:
|
|
572
|
+
return False
|
|
573
|
+
lines = text.split("\n")
|
|
574
|
+
if not lines or lines[0].strip() != "---":
|
|
575
|
+
return False
|
|
576
|
+
try:
|
|
577
|
+
close = next(i for i in range(1, len(lines)) if lines[i].strip() == "---")
|
|
578
|
+
except StopIteration:
|
|
579
|
+
return False
|
|
580
|
+
remaining = dict(updates)
|
|
581
|
+
for i in range(1, close):
|
|
582
|
+
key = lines[i].split(":", 1)[0].strip()
|
|
583
|
+
if key in remaining:
|
|
584
|
+
lines[i] = f"{key}: {json.dumps(remaining.pop(key), ensure_ascii=False)}"
|
|
585
|
+
# A field the original write omitted is appended rather than dropped, so a
|
|
586
|
+
# document ingested before a field existed still gains it.
|
|
587
|
+
for key, value in remaining.items():
|
|
588
|
+
lines.insert(close, f"{key}: {json.dumps(value, ensure_ascii=False)}")
|
|
589
|
+
close += 1
|
|
590
|
+
atomic_text(path, "\n".join(lines))
|
|
591
|
+
return True
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def reresolve_source(args: argparse.Namespace) -> dict:
|
|
595
|
+
"""Re-derive placement for documents already ingested.
|
|
596
|
+
|
|
597
|
+
Placement is written once, at ingest, and never revisited. That is right
|
|
598
|
+
for content — the text of a document does not change because a rule did —
|
|
599
|
+
but wrong for the rules themselves. Correcting a publisher profile changed
|
|
600
|
+
nothing about the corpus it governs: `add` skips on the content hash and
|
|
601
|
+
returns before it ever consults the profile, so it reported every document
|
|
602
|
+
unchanged while the corrected rule went unapplied. The only way to apply a
|
|
603
|
+
fix was to re-extract thousands of documents, redoing OCR to recompute a
|
|
604
|
+
field that depends on nothing but the filename.
|
|
605
|
+
|
|
606
|
+
So this re-runs the resolver alone. No file is reopened, no text is
|
|
607
|
+
re-extracted; the stored path is all the resolver ever needed.
|
|
608
|
+
|
|
609
|
+
Evidence precedence is the same as at ingest, and is the reason this is
|
|
610
|
+
safe to run at any time: a value the operator stated, or one the
|
|
611
|
+
organisation's own catalogue recorded, is never overwritten by an
|
|
612
|
+
inference. Only a placement this resolver derived — or the absence of one —
|
|
613
|
+
is open to revision.
|
|
614
|
+
"""
|
|
615
|
+
config = load_config()
|
|
616
|
+
source_id = slug(args.source)
|
|
617
|
+
directory = source_dir(source_id)
|
|
618
|
+
manifest = load_manifest(directory)
|
|
619
|
+
records = manifest.get("documents") or []
|
|
620
|
+
if not records:
|
|
621
|
+
return {"source": source_id, "error": "no manifest, or no documents in it",
|
|
622
|
+
"metadata_root": str(directory)}
|
|
623
|
+
|
|
624
|
+
profile = publisher_profile()
|
|
625
|
+
if profile is None:
|
|
626
|
+
return {"source": source_id,
|
|
627
|
+
"error": "no publisher-profile.json found — nothing to resolve against"}
|
|
628
|
+
key = source_profile.boundary_key(profile)
|
|
629
|
+
if not key:
|
|
630
|
+
return {"source": source_id, "error": "profile declares no boundary key"}
|
|
631
|
+
|
|
632
|
+
docs_dir = data_root() / "sources" / source_id / "documents"
|
|
633
|
+
placed = rezoned = identified = unchanged = protected = missing = 0
|
|
634
|
+
changes: list = []
|
|
635
|
+
|
|
636
|
+
for record in records:
|
|
637
|
+
relative = record.get("source_path")
|
|
638
|
+
if not relative:
|
|
639
|
+
unchanged += 1
|
|
640
|
+
continue
|
|
641
|
+
boundaries = dict(record.get("boundaries") or {})
|
|
642
|
+
provenance = dict(record.get("provenance") or {})
|
|
643
|
+
catalogued = provenance.get("from_catalogue") or {}
|
|
644
|
+
|
|
645
|
+
# The same evidence the original ingest had, re-read from the record so
|
|
646
|
+
# a document identified only by its catalogue row resolves here too.
|
|
647
|
+
catalogue_url, catalogue_institution = catalogue_provenance(catalogued)
|
|
648
|
+
resolved = source_profile.resolve(
|
|
649
|
+
profile, url=catalogue_url, institution=catalogue_institution,
|
|
650
|
+
filename=relative)
|
|
651
|
+
|
|
652
|
+
# Placement and identification are decided separately. A document can
|
|
653
|
+
# become identifiable — a journal index naming its publisher — without
|
|
654
|
+
# that saying anything about which region it applies to, and reporting
|
|
655
|
+
# only the placement would call that document unchanged when the thing
|
|
656
|
+
# that decides how far to trust it has just been settled.
|
|
657
|
+
current = boundaries.get(key)
|
|
658
|
+
# Anything the operator stated or the catalogue recorded outranks an
|
|
659
|
+
# inference and is left exactly as it is.
|
|
660
|
+
derived_here = (provenance.get("derived") is True
|
|
661
|
+
and provenance.get("applied_boundary") == key)
|
|
662
|
+
zone_open = not ((current and not derived_here) or catalogued.get(key))
|
|
663
|
+
zone_changes = bool(zone_open and resolved.zone and resolved.zone != current)
|
|
664
|
+
|
|
665
|
+
# A tier the catalogue stated is likewise not ours to revise.
|
|
666
|
+
class_open = not catalogued.get("tier")
|
|
667
|
+
class_changes = bool(
|
|
668
|
+
class_open and resolved.publisher_class
|
|
669
|
+
and (resolved.publisher_class != provenance.get("publisher_class")
|
|
670
|
+
or resolved.tier != provenance.get("tier")))
|
|
671
|
+
|
|
672
|
+
if not zone_changes and not class_changes:
|
|
673
|
+
if not zone_open and not class_open:
|
|
674
|
+
protected += 1
|
|
675
|
+
elif current and not zone_open:
|
|
676
|
+
protected += 1
|
|
677
|
+
else:
|
|
678
|
+
unchanged += 1
|
|
679
|
+
continue
|
|
680
|
+
|
|
681
|
+
# The fresh resolution replaces the recorded inference whenever either
|
|
682
|
+
# half moved. Refreshing only on a class change once left a re-zoned
|
|
683
|
+
# document carrying the confidence and notes of the resolution it no
|
|
684
|
+
# longer had — a record that reported an inferred national zone with
|
|
685
|
+
# the confidence of a precise one.
|
|
686
|
+
updated_provenance = dict(provenance)
|
|
687
|
+
updated_provenance.update(resolved.as_dict())
|
|
688
|
+
if class_changes:
|
|
689
|
+
identified += 1
|
|
690
|
+
if catalogued:
|
|
691
|
+
updated_provenance["from_catalogue"] = catalogued
|
|
692
|
+
if zone_changes:
|
|
693
|
+
updated_provenance["applied_boundary"] = key
|
|
694
|
+
updated_provenance["derived"] = True
|
|
695
|
+
boundaries[key] = resolved.zone
|
|
696
|
+
if current:
|
|
697
|
+
rezoned += 1
|
|
698
|
+
else:
|
|
699
|
+
placed += 1
|
|
700
|
+
|
|
701
|
+
changes.append({"id": record["id"], "source_path": relative,
|
|
702
|
+
"zone": {"from": current, "to": boundaries.get(key)},
|
|
703
|
+
"class": {"from": provenance.get("publisher_class"),
|
|
704
|
+
"to": updated_provenance.get("publisher_class")},
|
|
705
|
+
"matched_by": resolved.matched_by})
|
|
706
|
+
|
|
707
|
+
if args.dry_run:
|
|
708
|
+
continue
|
|
709
|
+
record["boundaries"] = boundaries
|
|
710
|
+
record["provenance"] = updated_provenance
|
|
711
|
+
file_path = record.get("file_path")
|
|
712
|
+
target = (data_root() / file_path) if file_path else (docs_dir / f"{record['id']}.md")
|
|
713
|
+
if not rewrite_frontmatter(target, {"boundaries": boundaries,
|
|
714
|
+
"provenance": updated_provenance}):
|
|
715
|
+
missing += 1
|
|
716
|
+
|
|
717
|
+
if not args.dry_run and (placed or rezoned or identified):
|
|
718
|
+
manifest["documents"] = sorted(records, key=lambda d: d["id"])
|
|
719
|
+
atomic_json(directory / "manifest.json", manifest)
|
|
720
|
+
|
|
721
|
+
return {
|
|
722
|
+
"org": slug(config.get("slug") or "egregore"),
|
|
723
|
+
"source": source_id,
|
|
724
|
+
"dry_run": bool(args.dry_run),
|
|
725
|
+
"documents": len(records),
|
|
726
|
+
"placed": placed,
|
|
727
|
+
"rezoned": rezoned,
|
|
728
|
+
"identified": identified,
|
|
729
|
+
"unchanged": unchanged,
|
|
730
|
+
"protected": protected,
|
|
731
|
+
"frontmatter_not_found": missing,
|
|
732
|
+
"examples": changes[: max(0, args.examples)],
|
|
733
|
+
"metadata_root": str(directory),
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
def cmd_reresolve(args: argparse.Namespace) -> int:
|
|
738
|
+
result = reresolve_source(args)
|
|
739
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
740
|
+
return 1 if result.get("error") else 0
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
def selection_relative_path(value: object) -> str:
|
|
744
|
+
if not isinstance(value, str) or not value or "\x00" in value or "\\" in value:
|
|
745
|
+
raise ValueError("selection contains an invalid relative path")
|
|
746
|
+
candidate = Path(value)
|
|
747
|
+
if candidate.is_absolute() or any(part in {"", ".", ".."} or part.startswith(".") for part in candidate.parts):
|
|
748
|
+
raise ValueError(f"selection path must be visible and relative: {value}")
|
|
749
|
+
return candidate.as_posix()
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
def validate_selection(path: Path) -> tuple[dict, Path]:
|
|
753
|
+
manifest_path = path.resolve()
|
|
754
|
+
staging_base = staging_root()
|
|
755
|
+
if manifest_path.name != "selection.json" or manifest_path.parent.parent != staging_base:
|
|
756
|
+
raise ValueError("selection manifest must be inside the local ingest staging root")
|
|
757
|
+
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
758
|
+
if manifest.get("schema") != SELECTION_SCHEMA:
|
|
759
|
+
raise ValueError("unsupported selection manifest schema")
|
|
760
|
+
if "org" in manifest:
|
|
761
|
+
raise ValueError("selection manifest cannot supply an org")
|
|
762
|
+
if manifest.get("session_id") != manifest_path.parent.name:
|
|
763
|
+
raise ValueError("selection session id does not match its staging directory")
|
|
764
|
+
if Path(str(manifest.get("staging_root", ""))).resolve() != manifest_path.parent:
|
|
765
|
+
raise ValueError("selection staging root does not match the manifest location")
|
|
766
|
+
|
|
767
|
+
source = manifest.get("source")
|
|
768
|
+
if not isinstance(source, dict) or "org" in source:
|
|
769
|
+
raise ValueError("selection source contract is invalid")
|
|
770
|
+
source_id = slug(str(source.get("id", "")))
|
|
771
|
+
if source_id != source.get("id"):
|
|
772
|
+
raise ValueError("selection source id must already be a normalized slug")
|
|
773
|
+
name = str(source.get("name", "")).strip()
|
|
774
|
+
if not name or len(name) > 200:
|
|
775
|
+
raise ValueError("selection source name is invalid")
|
|
776
|
+
if source.get("kind") != "files":
|
|
777
|
+
raise ValueError("local selections must use source kind files")
|
|
778
|
+
boundaries = source.get("boundaries", {})
|
|
779
|
+
if not isinstance(boundaries, dict):
|
|
780
|
+
raise ValueError("selection boundaries must be an object")
|
|
781
|
+
normalized_boundaries = parse_boundaries([f"{key}={value}" for key, value in boundaries.items()])
|
|
782
|
+
if normalized_boundaries != boundaries:
|
|
783
|
+
raise ValueError("selection boundary keys must already be normalized slugs")
|
|
784
|
+
|
|
785
|
+
files_root = manifest_path.parent / "files"
|
|
786
|
+
items = manifest.get("items")
|
|
787
|
+
if not isinstance(items, list) or not items:
|
|
788
|
+
raise ValueError("selection must contain at least one item")
|
|
789
|
+
declared: set[str] = set()
|
|
790
|
+
for item in items:
|
|
791
|
+
if not isinstance(item, dict):
|
|
792
|
+
raise ValueError("selection item must be an object")
|
|
793
|
+
relative = selection_relative_path(item.get("relative_path"))
|
|
794
|
+
if relative in declared:
|
|
795
|
+
raise ValueError(f"duplicate selection path: {relative}")
|
|
796
|
+
declared.add(relative)
|
|
797
|
+
expected = (files_root / relative).resolve()
|
|
798
|
+
if Path(str(item.get("local_path", ""))).resolve() != expected or files_root.resolve() not in expected.parents:
|
|
799
|
+
raise ValueError(f"selection item escaped its staging root: {relative}")
|
|
800
|
+
if not expected.is_file() or expected.is_symlink():
|
|
801
|
+
raise ValueError(f"selection item is missing or unsafe: {relative}")
|
|
802
|
+
content_hash, size_bytes = hash_file(expected)
|
|
803
|
+
if item.get("sha256") != content_hash or item.get("bytes") != size_bytes:
|
|
804
|
+
raise ValueError(f"selection item changed after confirmation: {relative}")
|
|
805
|
+
|
|
806
|
+
actual = {
|
|
807
|
+
candidate.relative_to(files_root).as_posix()
|
|
808
|
+
for candidate in files_root.rglob("*")
|
|
809
|
+
if candidate.is_file() and not candidate.is_symlink()
|
|
810
|
+
}
|
|
811
|
+
if actual != declared:
|
|
812
|
+
raise ValueError("staging directory does not match the confirmed selection")
|
|
813
|
+
return manifest, files_root
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
def registered_extractions(manifest: dict) -> dict[str, dict]:
|
|
817
|
+
session_root = Path(manifest["staging_root"])
|
|
818
|
+
directory = session_root / "extractions"
|
|
819
|
+
if not directory.exists():
|
|
820
|
+
return {}
|
|
821
|
+
declared = {item["relative_path"]: item for item in manifest["items"]}
|
|
822
|
+
result: dict[str, dict] = {}
|
|
823
|
+
for sidecar_path in sorted(directory.glob("*.json")):
|
|
824
|
+
sidecar = json.loads(sidecar_path.read_text(encoding="utf-8"))
|
|
825
|
+
if sidecar.get("schema") != EXTRACTION_SCHEMA:
|
|
826
|
+
raise ValueError(f"unsupported extraction sidecar: {sidecar_path.name}")
|
|
827
|
+
relative = selection_relative_path(sidecar.get("relative_path"))
|
|
828
|
+
item = declared.get(relative)
|
|
829
|
+
if not item:
|
|
830
|
+
raise ValueError(f"extraction does not belong to this selection: {relative}")
|
|
831
|
+
if sidecar_path.name != f"{digest(relative)}.json":
|
|
832
|
+
raise ValueError(f"extraction sidecar has an invalid name: {relative}")
|
|
833
|
+
if sidecar.get("source_sha256") != item["sha256"]:
|
|
834
|
+
raise ValueError(f"extraction source hash does not match: {relative}")
|
|
835
|
+
extractor = str(sidecar.get("extractor", "")).strip()
|
|
836
|
+
text = sidecar.get("text")
|
|
837
|
+
if not extractor or len(extractor) > 100 or not isinstance(text, str) or not text.strip():
|
|
838
|
+
raise ValueError(f"extraction sidecar is incomplete: {relative}")
|
|
839
|
+
result[relative] = {"extractor": extractor, "text": text}
|
|
840
|
+
return result
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
def cmd_register_extraction(args: argparse.Namespace) -> int:
|
|
844
|
+
manifest, _ = validate_selection(Path(args.manifest))
|
|
845
|
+
relative = selection_relative_path(args.relative_path)
|
|
846
|
+
item = next((row for row in manifest["items"] if row["relative_path"] == relative), None)
|
|
847
|
+
if not item:
|
|
848
|
+
raise ValueError(f"file is not part of this selection: {relative}")
|
|
849
|
+
extractor = args.extractor.strip()
|
|
850
|
+
if not extractor or len(extractor) > 100:
|
|
851
|
+
raise ValueError("extractor name is invalid")
|
|
852
|
+
text = sys.stdin.read()
|
|
853
|
+
if not text.strip():
|
|
854
|
+
raise ValueError("registered extraction is empty")
|
|
855
|
+
directory = Path(manifest["staging_root"]) / "extractions"
|
|
856
|
+
path = directory / f"{digest(relative)}.json"
|
|
857
|
+
atomic_json(path, {
|
|
858
|
+
"schema": EXTRACTION_SCHEMA,
|
|
859
|
+
"relative_path": relative,
|
|
860
|
+
"source_sha256": item["sha256"],
|
|
861
|
+
"extractor": extractor,
|
|
862
|
+
"text": text,
|
|
863
|
+
})
|
|
864
|
+
print(json.dumps({"registered": relative, "extractor": extractor, "sidecar": str(path)}))
|
|
865
|
+
return 0
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
def cmd_add_selection(args: argparse.Namespace) -> int:
|
|
869
|
+
manifest, files_root = validate_selection(Path(args.manifest))
|
|
870
|
+
source = manifest["source"]
|
|
871
|
+
add_args = argparse.Namespace(
|
|
872
|
+
path=str(files_root),
|
|
873
|
+
source=source["id"],
|
|
874
|
+
name=source["name"],
|
|
875
|
+
kind="files",
|
|
876
|
+
boundary=[f"{key}={value}" for key, value in source.get("boundaries", {}).items()],
|
|
877
|
+
prune=bool(source.get("authoritative_snapshot", False)),
|
|
878
|
+
extractions=registered_extractions(manifest),
|
|
879
|
+
)
|
|
880
|
+
summary = add_path(add_args)
|
|
881
|
+
cleaned = summary["skipped"] == 0
|
|
882
|
+
if cleaned:
|
|
883
|
+
shutil.rmtree(Path(manifest["staging_root"]), ignore_errors=True)
|
|
884
|
+
summary["selection_files"] = len(manifest["items"])
|
|
885
|
+
summary["staging_cleaned"] = cleaned
|
|
886
|
+
if not cleaned:
|
|
887
|
+
summary["selection_path"] = str(Path(args.manifest).resolve())
|
|
888
|
+
print(json.dumps(summary, indent=2))
|
|
889
|
+
return 0
|
|
890
|
+
|
|
891
|
+
|
|
892
|
+
def all_sources() -> list[tuple[dict, dict]]:
|
|
893
|
+
base = metadata_root() / "sources"
|
|
894
|
+
if not base.exists():
|
|
895
|
+
return []
|
|
896
|
+
result = []
|
|
897
|
+
for manifest_path in sorted(base.glob("*/manifest.json")):
|
|
898
|
+
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
899
|
+
result.append((manifest.get("source", {}), manifest))
|
|
900
|
+
return result
|
|
901
|
+
|
|
902
|
+
|
|
903
|
+
def boundaries_match(actual: dict, required: dict) -> bool:
|
|
904
|
+
return all(str(actual.get(key, "")) == str(value) for key, value in required.items())
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
def cmd_filter_results(args: argparse.Namespace) -> int:
|
|
908
|
+
required = parse_boundaries(args.boundary)
|
|
909
|
+
allowed: set[str] = set()
|
|
910
|
+
for source, manifest in all_sources():
|
|
911
|
+
if args.source and source.get("id") != slug(args.source):
|
|
912
|
+
continue
|
|
913
|
+
if not boundaries_match(source.get("boundaries", {}), required):
|
|
914
|
+
continue
|
|
915
|
+
allowed.update(d["file_path"] for d in manifest.get("documents", []) if d.get("status") != "extraction_failed")
|
|
916
|
+
results = json.load(sys.stdin)
|
|
917
|
+
filtered = []
|
|
918
|
+
for item in results if isinstance(results, list) else []:
|
|
919
|
+
file_name = re.sub(r"^qmd://[^/]+/", "", item.get("file", ""))
|
|
920
|
+
if file_name in allowed:
|
|
921
|
+
filtered.append(item)
|
|
922
|
+
if len(filtered) >= args.limit:
|
|
923
|
+
break
|
|
924
|
+
print(json.dumps(filtered, ensure_ascii=False))
|
|
925
|
+
return 0
|
|
926
|
+
|
|
927
|
+
|
|
928
|
+
def cmd_status(_: argparse.Namespace) -> int:
|
|
929
|
+
sources = all_sources()
|
|
930
|
+
documents = sum(len(m.get("documents", [])) for _, m in sources)
|
|
931
|
+
chunks_count = sum(len(d.get("chunks", [])) for _, m in sources for d in m.get("documents", []))
|
|
932
|
+
errors = sum(len(m.get("errors", [])) for _, m in sources)
|
|
933
|
+
cached = sum(1 for _, m in sources for d in m.get("documents", []) if (data_root() / d["file_path"]).is_file())
|
|
934
|
+
state_dir = data_root() / ".state"
|
|
935
|
+
pending = []
|
|
936
|
+
if state_dir.exists():
|
|
937
|
+
pending = [json.loads(path.read_text(encoding="utf-8")) for path in sorted(state_dir.glob("*.json"))]
|
|
938
|
+
print(json.dumps({"org": slug(load_config().get("slug") or "egregore"), "metadata_root": str(metadata_root()), "data_root": str(data_root()), "staging_root": str(staging_root()), "sources": len(sources), "documents": documents, "cached_documents": cached, "chunks": chunks_count, "errors": errors, "pending_recovery": pending}, indent=2))
|
|
939
|
+
return 0
|
|
940
|
+
|
|
941
|
+
|
|
942
|
+
def cmd_journal(args: argparse.Namespace) -> int:
|
|
943
|
+
path = data_root() / ".state" / f"{slug(args.source)}.json"
|
|
944
|
+
if args.clear:
|
|
945
|
+
path.unlink(missing_ok=True)
|
|
946
|
+
return 0
|
|
947
|
+
detail = json.loads(args.detail) if args.detail else {}
|
|
948
|
+
atomic_json(path, {
|
|
949
|
+
"source": slug(args.source),
|
|
950
|
+
"phase": args.phase,
|
|
951
|
+
"detail": detail,
|
|
952
|
+
"updated_at": datetime.now(timezone.utc).isoformat(),
|
|
953
|
+
})
|
|
954
|
+
return 0
|
|
955
|
+
|
|
956
|
+
|
|
957
|
+
def parser() -> argparse.ArgumentParser:
|
|
958
|
+
result = argparse.ArgumentParser(description=__doc__)
|
|
959
|
+
sub = result.add_subparsers(dest="command", required=True)
|
|
960
|
+
add = sub.add_parser("add")
|
|
961
|
+
add.add_argument("path")
|
|
962
|
+
add.add_argument("--source", required=True, help="stable connector/dataset id")
|
|
963
|
+
add.add_argument("--name")
|
|
964
|
+
add.add_argument("--kind", default="files")
|
|
965
|
+
add.add_argument("--boundary", action="append", default=[])
|
|
966
|
+
add.add_argument("--prune", action="store_true", help="tombstone files absent from this authoritative directory snapshot")
|
|
967
|
+
add.set_defaults(func=cmd_add)
|
|
968
|
+
reresolve = sub.add_parser(
|
|
969
|
+
"reresolve",
|
|
970
|
+
help="re-derive placement from the current profile, without re-reading any document")
|
|
971
|
+
reresolve.add_argument("--source", required=True, help="source id to re-resolve")
|
|
972
|
+
reresolve.add_argument("--dry-run", action="store_true",
|
|
973
|
+
help="report what would change and write nothing")
|
|
974
|
+
reresolve.add_argument("--examples", type=int, default=10,
|
|
975
|
+
help="how many changed documents to list (default 10)")
|
|
976
|
+
reresolve.set_defaults(func=cmd_reresolve)
|
|
977
|
+
add_selection = sub.add_parser("add-selection")
|
|
978
|
+
add_selection.add_argument("manifest")
|
|
979
|
+
add_selection.set_defaults(func=cmd_add_selection)
|
|
980
|
+
extraction = sub.add_parser("register-extraction")
|
|
981
|
+
extraction.add_argument("manifest")
|
|
982
|
+
extraction.add_argument("relative_path")
|
|
983
|
+
extraction.add_argument("--extractor", required=True)
|
|
984
|
+
extraction.set_defaults(func=cmd_register_extraction)
|
|
985
|
+
filt = sub.add_parser("filter-results")
|
|
986
|
+
filt.add_argument("--source")
|
|
987
|
+
filt.add_argument("--boundary", action="append", default=[])
|
|
988
|
+
filt.add_argument("--limit", type=int, default=8)
|
|
989
|
+
filt.set_defaults(func=cmd_filter_results)
|
|
990
|
+
status = sub.add_parser("status")
|
|
991
|
+
status.set_defaults(func=cmd_status)
|
|
992
|
+
journal = sub.add_parser("journal")
|
|
993
|
+
journal.add_argument("--source", required=True)
|
|
994
|
+
journal.add_argument("--phase", default="pending")
|
|
995
|
+
journal.add_argument("--detail")
|
|
996
|
+
journal.add_argument("--clear", action="store_true")
|
|
997
|
+
journal.set_defaults(func=cmd_journal)
|
|
998
|
+
return result
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
def main() -> int:
|
|
1002
|
+
try:
|
|
1003
|
+
args = parser().parse_args()
|
|
1004
|
+
return args.func(args)
|
|
1005
|
+
except (ValueError, OSError, json.JSONDecodeError) as exc:
|
|
1006
|
+
print(f"ingest: {exc}", file=sys.stderr)
|
|
1007
|
+
return 2
|
|
1008
|
+
|
|
1009
|
+
|
|
1010
|
+
if __name__ == "__main__":
|
|
1011
|
+
raise SystemExit(main())
|