@pugi/cli 0.1.0-beta.8 → 0.1.0-beta.87
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/CHANGELOG.md +96 -0
- package/THIRD_PARTY_NOTICES.md +40 -0
- package/assets/pugi-prozr2-mascot.ansi +9 -0
- package/bin/run.js +33 -1
- package/dist/commands/deploy.js +40 -40
- package/dist/commands/flatten.js +191 -0
- package/dist/commands/jobs-watch.js +201 -0
- package/dist/commands/jobs.js +42 -27
- package/dist/commands/smoke.js +133 -0
- package/dist/core/agent-progress/cleanup.js +134 -0
- package/dist/core/agent-progress/schema.js +144 -0
- package/dist/core/agent-progress/writer.js +101 -0
- package/dist/core/agents/adaptive-router.js +330 -0
- package/dist/core/agents/query-decomposer.js +297 -0
- package/dist/core/agents/registry.js +2 -2
- package/dist/core/approvals/shortcut-resolver.js +98 -0
- package/dist/core/artifact-chain/dispatcher.js +148 -0
- package/dist/core/artifact-chain/exporter.js +164 -0
- package/dist/core/artifact-chain/state.js +243 -0
- package/dist/core/artifact-chain/steps.js +169 -0
- package/dist/core/ask-user/question.js +92 -0
- package/dist/core/audit/audit-trail.js +275 -0
- package/dist/core/auth/ensure-authenticated.js +129 -0
- package/dist/core/auth/env-provider.js +238 -0
- package/dist/core/auto-open-browser.js +4 -4
- package/dist/core/auto-update/channels.js +122 -0
- package/dist/core/auto-update/checker.js +241 -0
- package/dist/core/auto-update/state.js +235 -0
- package/dist/core/bare-mode/index.js +107 -0
- package/dist/core/bash/redirect.js +281 -0
- package/dist/core/bash-classifier.js +436 -40
- package/dist/core/checkpoint/resumer.js +149 -0
- package/dist/core/checkpoint/rewinder.js +291 -0
- package/dist/core/checkpoints/shadow-git.js +670 -0
- package/dist/core/citations/parser.js +109 -0
- package/dist/core/classifier/yolo-classifier.js +88 -0
- package/dist/core/codegraph/decision-store.js +248 -0
- package/dist/core/codegraph/detect-repo.js +459 -0
- package/dist/core/codegraph/install.js +134 -0
- package/dist/core/codegraph/offer-hook.js +220 -0
- package/dist/core/compact/auto-trigger.js +96 -0
- package/dist/core/compact/buffer-rewriter.js +115 -0
- package/dist/core/compact/summarizer.js +208 -0
- package/dist/core/compact/token-counter.js +108 -0
- package/dist/core/consensus/anvil-fanout.js +25 -25
- package/dist/core/consensus/diff-capture.js +121 -12
- package/dist/core/consensus/rubric.js +21 -21
- package/dist/core/context/builder.js +6 -6
- package/dist/core/context/compaction-events.js +8 -8
- package/dist/core/context/compaction.js +31 -31
- package/dist/core/context/index.js +15 -8
- package/dist/core/context/invariants.js +51 -51
- package/dist/core/context/markdown-loader.js +28 -10
- package/dist/core/context/markdown-traverse.js +255 -0
- package/dist/core/context/pugiignore.js +41 -41
- package/dist/core/context/repo-skeleton.js +37 -37
- package/dist/core/context/tool-eviction.js +55 -0
- package/dist/core/context/watcher.js +32 -32
- package/dist/core/context/working-set.js +23 -23
- package/dist/core/coordinator/agent-tools.js +77 -0
- package/dist/core/coordinator/agent-toolset.js +65 -0
- package/dist/core/coordinator/fsm.js +73 -0
- package/dist/core/coordinator/mode-fsm.js +70 -0
- package/dist/core/cost/rate-card.js +129 -0
- package/dist/core/cost/tracker.js +221 -0
- package/dist/core/credentials.js +12 -12
- package/dist/core/cron/scheduler.js +138 -0
- package/dist/core/denial-tracking/index.js +8 -0
- package/dist/core/denial-tracking/state.js +264 -0
- package/dist/core/diagnostics/probe-runner.js +93 -0
- package/dist/core/diagnostics/probes/api.js +46 -0
- package/dist/core/diagnostics/probes/auth.js +93 -0
- package/dist/core/diagnostics/probes/bare-mode.js +42 -0
- package/dist/core/diagnostics/probes/cli-version.js +127 -0
- package/dist/core/diagnostics/probes/config.js +72 -0
- package/dist/core/diagnostics/probes/denial-tracking.js +57 -0
- package/dist/core/diagnostics/probes/disk.js +81 -0
- package/dist/core/diagnostics/probes/engine-live.js +46 -0
- package/dist/core/diagnostics/probes/git.js +65 -0
- package/dist/core/diagnostics/probes/hooks.js +118 -0
- package/dist/core/diagnostics/probes/mcp.js +75 -0
- package/dist/core/diagnostics/probes/node.js +59 -0
- package/dist/core/diagnostics/probes/pnpm.js +36 -0
- package/dist/core/diagnostics/probes/pugi-md.js +89 -0
- package/dist/core/diagnostics/probes/sandbox.js +40 -0
- package/dist/core/diagnostics/probes/session.js +74 -0
- package/dist/core/diagnostics/probes/status-snapshot.js +488 -0
- package/dist/core/diagnostics/probes/workspace.js +63 -0
- package/dist/core/diagnostics/types.js +70 -0
- package/dist/core/dispatch/cache-cleanup.js +197 -0
- package/dist/core/dispatch/cache-handoff.js +295 -0
- package/dist/core/edits/apply-patch-layer-e.js +189 -0
- package/dist/core/edits/dispatch.js +293 -7
- package/dist/core/edits/format-matrix.js +26 -0
- package/dist/core/edits/fuzzy-ladder.js +650 -0
- package/dist/core/edits/index.js +3 -1
- package/dist/core/edits/journal.js +199 -0
- package/dist/core/edits/layer-a-apply.js +15 -15
- package/dist/core/edits/layer-a-fuzzy-apply.js +198 -0
- package/dist/core/edits/layer-b-apply.js +9 -9
- package/dist/core/edits/layer-c-apply.js +6 -6
- package/dist/core/edits/layer-d-ast.js +557 -14
- package/dist/core/edits/marker-parser.js +12 -12
- package/dist/core/edits/security-gate.js +27 -27
- package/dist/core/edits/verify-hook.js +273 -0
- package/dist/core/edits/worktree.js +322 -0
- package/dist/core/engine/anvil-client.js +140 -26
- package/dist/core/engine/auto-compact.js +179 -0
- package/dist/core/engine/budgets.js +186 -0
- package/dist/core/engine/context-prefix.js +155 -0
- package/dist/core/engine/index.js +1 -1
- package/dist/core/engine/intensity.js +158 -0
- package/dist/core/engine/intent.js +260 -0
- package/dist/core/engine/native-pugi.js +1295 -227
- package/dist/core/engine/prompts.js +134 -16
- package/dist/core/engine/strip-internal-fields.js +124 -0
- package/dist/core/engine/tool-bridge.js +1295 -59
- package/dist/core/evaluation/golden-dataset.js +293 -0
- package/dist/core/feedback/queue.js +177 -0
- package/dist/core/feedback/submitter.js +145 -0
- package/dist/core/file-cache.js +113 -1
- package/dist/core/flatten/flatten-repo.js +439 -0
- package/dist/core/format/osc8-link.js +28 -0
- package/dist/core/hook-chains.js +392 -0
- package/dist/core/hooks/citation-verify-hook.js +138 -0
- package/dist/core/hooks/citation-verify.js +112 -0
- package/dist/core/hooks/events.js +44 -0
- package/dist/core/hooks/index.js +15 -0
- package/dist/core/hooks/registry.js +213 -0
- package/dist/core/hooks/runner.js +236 -0
- package/dist/core/hooks/v2/event-emitter.js +115 -0
- package/dist/core/hooks/v2/executor.js +282 -0
- package/dist/core/hooks/v2/index.js +25 -0
- package/dist/core/hooks/v2/lifecycle.js +104 -0
- package/dist/core/hooks/v2/loader.js +216 -0
- package/dist/core/hooks/v2/matcher.js +125 -0
- package/dist/core/hooks/v2/trust.js +143 -0
- package/dist/core/hooks/v2/types.js +86 -0
- package/dist/core/image/renderer.js +71 -0
- package/dist/core/init/detector.js +582 -0
- package/dist/core/init/template-renderer.js +242 -0
- package/dist/core/jobs/registry.js +18 -18
- package/dist/core/ledger/results-tsv.js +142 -0
- package/dist/core/log-discipline/stdout-redirect.js +51 -0
- package/dist/core/lsp/cache.js +105 -0
- package/dist/core/lsp/client.js +776 -0
- package/dist/core/lsp/language-detect.js +66 -0
- package/dist/core/lsp/post-edit-diagnostics.js +171 -0
- package/dist/core/lsp/symbol-tools.js +372 -0
- package/dist/core/mcp/client.js +97 -28
- package/dist/core/mcp/http-server.js +553 -0
- package/dist/core/mcp/orchestrator-tools.js +662 -0
- package/dist/core/mcp/permission.js +190 -0
- package/dist/core/mcp/registry.js +39 -17
- package/dist/core/mcp/server-tools.js +219 -0
- package/dist/core/mcp/server.js +397 -0
- package/dist/core/mcp/trust.js +10 -10
- package/dist/core/memory/dual-write.js +416 -0
- package/dist/core/memory/passive-extract.js +130 -0
- package/dist/core/memory/phase1-kinds.js +20 -0
- package/dist/core/memory/secret-scanner.js +304 -0
- package/dist/core/memory-sync/queue.js +170 -0
- package/dist/core/metrics/extract.js +113 -0
- package/dist/core/modes/roo-modes.js +68 -0
- package/dist/core/onboarding/ensure-initialized.js +133 -0
- package/dist/core/onboarding/marker.js +111 -0
- package/dist/core/onboarding/telemetry-state.js +108 -0
- package/dist/core/output-style/presets.js +176 -0
- package/dist/core/output-style/state.js +185 -0
- package/dist/core/path-security.js +287 -5
- package/dist/core/permission.js +82 -22
- package/dist/core/permissions/auto-classifier.js +124 -0
- package/dist/core/permissions/bash-parser.js +371 -0
- package/dist/core/permissions/circuit-breaker.js +83 -0
- package/dist/core/permissions/constrained-edit.js +91 -0
- package/dist/core/permissions/gate.js +278 -0
- package/dist/core/permissions/index.js +20 -0
- package/dist/core/permissions/mode.js +174 -0
- package/dist/core/permissions/network-egress.js +137 -0
- package/dist/core/permissions/state.js +241 -0
- package/dist/core/permissions/tool-class.js +93 -0
- package/dist/core/plan-mode/ui-state.js +51 -0
- package/dist/core/plans/plan-artifact.js +721 -0
- package/dist/core/policy-limits/etag-store.js +122 -0
- package/dist/core/prd-check/parser.js +215 -0
- package/dist/core/prd-check/reporter.js +127 -0
- package/dist/core/prd-check/session-review.js +557 -0
- package/dist/core/prd-check/verifiers.js +223 -0
- package/dist/core/prompt-cache/client-cache.js +99 -0
- package/dist/core/prompts/assembly.js +29 -0
- package/dist/core/prompts/registry.js +364 -0
- package/dist/core/pugi-md/cc-compat-rules.js +735 -0
- package/dist/core/pugi-md/context-injector.js +76 -0
- package/dist/core/pugi-md/walk-up.js +207 -0
- package/dist/core/python/uv-installer.js +270 -0
- package/dist/core/python/uv-resolver.js +83 -0
- package/dist/core/rate-limit/narrator.js +146 -0
- package/dist/core/recipes/cli-types.js +20 -0
- package/dist/core/recipes/loader.js +103 -0
- package/dist/core/recipes/runner.js +345 -0
- package/dist/core/recipes/schema.js +587 -0
- package/dist/core/release-notes/parser.js +241 -0
- package/dist/core/release-notes/state.js +116 -0
- package/dist/core/repl/ask.js +37 -37
- package/dist/core/repl/cancellation.js +26 -26
- package/dist/core/repl/cap-warning.js +4 -4
- package/dist/core/repl/clipboard-read.js +11 -11
- package/dist/core/repl/dispatch-fsm.js +12 -12
- package/dist/core/repl/history-search.js +15 -15
- package/dist/core/repl/history.js +28 -18
- package/dist/core/repl/kill-ring.js +5 -5
- package/dist/core/repl/model-pricing.js +135 -0
- package/dist/core/repl/privacy-banner.js +22 -22
- package/dist/core/repl/session.js +2157 -214
- package/dist/core/repl/slash-commands.js +533 -40
- package/dist/core/repl/store/index.js +1 -1
- package/dist/core/repl/store/jsonl-log.js +22 -22
- package/dist/core/repl/store/lockfile.js +10 -10
- package/dist/core/repl/store/session-store.js +136 -107
- package/dist/core/repl/store/types.js +15 -15
- package/dist/core/repl/store/uuid-v7.js +12 -12
- package/dist/core/repl/workspace-context.js +43 -21
- package/dist/core/repo-map/build.js +125 -0
- package/dist/core/repo-map/cache.js +185 -0
- package/dist/core/repo-map/extractor.js +254 -0
- package/dist/core/repo-map/formatter.js +145 -0
- package/dist/core/repo-map/page-rank.js +105 -0
- package/dist/core/repo-map/scanner.js +211 -0
- package/dist/core/retry-budget/budget.js +284 -0
- package/dist/core/retry-budget/index.js +5 -0
- package/dist/core/retry-budget/retry-cap.js +74 -0
- package/dist/core/routing/lead-worker.js +43 -0
- package/dist/core/routing/pre-flight-estimator.js +108 -0
- package/dist/core/runs/run-tree.js +103 -0
- package/dist/core/security/injection-scanner.js +367 -0
- package/dist/core/security/output-filter.js +418 -0
- package/dist/core/session/env-file.js +105 -0
- package/dist/core/session/section-budgets.js +140 -0
- package/dist/core/session.js +92 -0
- package/dist/core/settings.js +286 -5
- package/dist/core/share/formatter.js +271 -0
- package/dist/core/share/redactor.js +221 -0
- package/dist/core/share/uploader.js +267 -0
- package/dist/core/skills/defaults.js +457 -0
- package/dist/core/skills/loader.js +22 -22
- package/dist/core/skills/sources.js +27 -27
- package/dist/core/smoke/headless-driver.js +174 -0
- package/dist/core/smoke/orchestrator.js +194 -0
- package/dist/core/smoke/runner.js +238 -0
- package/dist/core/smoke/scenario-parser.js +316 -0
- package/dist/core/statusline.js +99 -0
- package/dist/core/subagents/dispatcher-real.js +600 -0
- package/dist/core/subagents/dispatcher.js +132 -43
- package/dist/core/subagents/index.js +19 -6
- package/dist/core/subagents/isolation-matrix.js +213 -0
- package/dist/core/subagents/spawn.js +19 -4
- package/dist/core/telemetry/emitter.js +229 -0
- package/dist/core/telemetry/queue.js +251 -0
- package/dist/core/theme/context.js +91 -0
- package/dist/core/theme/presets.js +228 -0
- package/dist/core/theme/state.js +181 -0
- package/dist/core/todos/invariant.js +10 -0
- package/dist/core/todos/state.js +177 -0
- package/dist/core/tool-schema/compressor.js +89 -0
- package/dist/core/transport/version-interceptor.js +166 -0
- package/dist/core/trust.js +2 -2
- package/dist/core/tui/thinking-block.js +64 -0
- package/dist/core/vim/keymap.js +288 -0
- package/dist/core/vim/state.js +92 -0
- package/dist/core/watch-markers/marker-watcher.js +133 -0
- package/dist/core/worktree-manager/cleanup.js +123 -0
- package/dist/core/worktree-manager/manager.js +303 -0
- package/dist/index.js +28 -0
- package/dist/runtime/bootstrap.js +190 -0
- package/dist/runtime/cli.js +4151 -489
- package/dist/runtime/commands/agents.js +30 -30
- package/dist/runtime/commands/budget.js +5 -5
- package/dist/runtime/commands/cancel.js +231 -0
- package/dist/runtime/commands/chain.js +489 -0
- package/dist/runtime/commands/codegraph-status.js +227 -0
- package/dist/runtime/commands/compact.js +297 -0
- package/dist/runtime/commands/config.js +32 -32
- package/dist/runtime/commands/cost.js +199 -0
- package/dist/runtime/commands/delegate.js +244 -13
- package/dist/runtime/commands/dispatch.js +126 -0
- package/dist/runtime/commands/doctor.js +579 -0
- package/dist/runtime/commands/feedback.js +184 -0
- package/dist/runtime/commands/hooks.js +184 -0
- package/dist/runtime/commands/init.js +254 -0
- package/dist/runtime/commands/lsp.js +368 -0
- package/dist/runtime/commands/mcp.js +879 -0
- package/dist/runtime/commands/memory.js +582 -0
- package/dist/runtime/commands/model.js +237 -0
- package/dist/runtime/commands/onboarding.js +275 -0
- package/dist/runtime/commands/patch.js +128 -0
- package/dist/runtime/commands/permissions.js +112 -0
- package/dist/runtime/commands/plan.js +143 -0
- package/dist/runtime/commands/prd-check.js +285 -0
- package/dist/runtime/commands/privacy.js +17 -17
- package/dist/runtime/commands/recipe.js +325 -0
- package/dist/runtime/commands/redo-blob-store.js +92 -0
- package/dist/runtime/commands/redo.js +361 -0
- package/dist/runtime/commands/release-notes.js +229 -0
- package/dist/runtime/commands/repo-map.js +95 -0
- package/dist/runtime/commands/report.js +299 -0
- package/dist/runtime/commands/resume.js +118 -0
- package/dist/runtime/commands/review-consensus.js +68 -53
- package/dist/runtime/commands/rewind.js +333 -0
- package/dist/runtime/commands/roster.js +14 -14
- package/dist/runtime/commands/sessions.js +163 -0
- package/dist/runtime/commands/share.js +316 -0
- package/dist/runtime/commands/skills.js +31 -31
- package/dist/runtime/commands/status.js +186 -0
- package/dist/runtime/commands/stickers.js +82 -0
- package/dist/runtime/commands/style.js +194 -0
- package/dist/runtime/commands/theme.js +196 -0
- package/dist/runtime/commands/undo.js +54 -22
- package/dist/runtime/commands/update.js +289 -0
- package/dist/runtime/commands/vim.js +140 -0
- package/dist/runtime/commands/worktree.js +177 -0
- package/dist/runtime/commands/worktrees.js +155 -0
- package/dist/runtime/headless-repl.js +195 -0
- package/dist/runtime/headless.js +543 -0
- package/dist/runtime/load-hooks-or-exit.js +71 -0
- package/dist/runtime/plan-decompose.js +531 -0
- package/dist/runtime/update-check.js +28 -28
- package/dist/runtime/version.js +65 -0
- package/dist/skills/bundled/batch.js +617 -0
- package/dist/skills/bundled/index.js +45 -0
- package/dist/skills/bundled/loop.js +358 -0
- package/dist/skills/bundled/remember.js +383 -0
- package/dist/skills/bundled/simplify.js +289 -0
- package/dist/skills/bundled/skillify.js +373 -0
- package/dist/skills/bundled/stuck.js +558 -0
- package/dist/skills/bundled/verify.js +439 -0
- package/dist/testing/vcr.js +486 -0
- package/dist/tools/agent-tool.js +229 -0
- package/dist/tools/apply-patch.js +556 -0
- package/dist/tools/ask-user-question.js +222 -0
- package/dist/tools/ask-user.js +115 -0
- package/dist/tools/bash.js +623 -45
- package/dist/tools/brief.js +224 -0
- package/dist/tools/enter-worktree.js +250 -0
- package/dist/tools/exit-worktree.js +147 -0
- package/dist/tools/file-tools.js +161 -44
- package/dist/tools/lsp-tools.js +189 -0
- package/dist/tools/mcp-tool.js +260 -0
- package/dist/tools/multi-edit.js +361 -0
- package/dist/tools/powershell.js +268 -0
- package/dist/tools/registry.js +85 -0
- package/dist/tools/skill-tool.js +96 -0
- package/dist/tools/sleep.js +99 -0
- package/dist/tools/synthetic-output.js +133 -0
- package/dist/tools/tasks.js +208 -0
- package/dist/tools/todo-write.js +184 -0
- package/dist/tools/verify-plan-execution.js +295 -0
- package/dist/tools/web-fetch-injection-scanner.js +207 -0
- package/dist/tools/web-fetch.js +195 -10
- package/dist/tools/web-search.js +458 -0
- package/dist/tui/agent-progress-card.js +111 -0
- package/dist/tui/agent-tree.js +11 -1
- package/dist/tui/ask-modal.js +14 -14
- package/dist/tui/ask-user-question-prompt.js +203 -0
- package/dist/tui/compact-banner.js +81 -0
- package/dist/tui/conversation-pane.js +85 -11
- package/dist/tui/cost-table.js +111 -0
- package/dist/tui/device-flow.js +2 -2
- package/dist/tui/doctor-table.js +46 -0
- package/dist/tui/feedback-prompt.js +156 -0
- package/dist/tui/input-box.js +247 -32
- package/dist/tui/login-picker.js +3 -3
- package/dist/tui/markdown-render.js +6 -6
- package/dist/tui/onboarding-wizard.js +240 -0
- package/dist/tui/permissions-picker.js +86 -0
- package/dist/tui/render.js +35 -0
- package/dist/tui/repl-render.js +332 -54
- package/dist/tui/repl-splash-art.js +16 -16
- package/dist/tui/repl-splash-mascot.js +48 -24
- package/dist/tui/repl-splash.js +22 -22
- package/dist/tui/repl.js +124 -44
- package/dist/tui/slash-palette.js +6 -6
- package/dist/tui/splash.js +2 -2
- package/dist/tui/status-bar.js +109 -31
- package/dist/tui/status-table.js +7 -0
- package/dist/tui/stickers-art.js +136 -0
- package/dist/tui/style-table.js +28 -0
- package/dist/tui/theme-table.js +29 -0
- package/dist/tui/thinking-spinner.js +123 -0
- package/dist/tui/tool-stream-pane.js +53 -4
- package/dist/tui/update-banner.js +27 -2
- package/dist/tui/vim-input.js +267 -0
- package/dist/tui/welcome-banner.js +107 -0
- package/dist/tui/welcome-data.js +293 -0
- package/dist/tui/workspace-context.js +2 -2
- package/docs/examples/codegraph.mcp.json +10 -0
- package/package.json +23 -6
- package/test/scenarios/codegen-create-file.scenario.txt +13 -0
- package/test/scenarios/compact-force.scenario.txt +11 -0
- package/test/scenarios/identity.scenario.txt +11 -0
- package/test/scenarios/persona-handoff.scenario.txt +11 -0
- package/test/scenarios/walkback.scenario.txt +12 -0
- package/dist/core/engine/compaction-hook.js +0 -154
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Workspace context resolver — Sprint
|
|
2
|
+
* Workspace context resolver — Sprint wave 4.
|
|
3
3
|
*
|
|
4
4
|
* Reads the operator's cwd and synthesises the workspace bundle the CLI
|
|
5
|
-
* forwards to admin-api on POST /api/pugi/sessions.
|
|
5
|
+
* forwards to admin-api on POST /api/pugi/sessions. Pugi's prompt v1.1
|
|
6
6
|
* consumes the bundle so "what repo is this?" / "а изучи репо…" answers
|
|
7
7
|
* from the live cwd instead of bouncing back "репо не привязано" (CEO
|
|
8
|
-
* dogfood
|
|
8
|
+
* dogfood).
|
|
9
9
|
*
|
|
10
10
|
* Three fields:
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* - `workspaceCwd` — absolute path the CLI was launched from.
|
|
13
|
+
* - `workspaceSlug` — a stable short identifier (slugForCwd).
|
|
14
|
+
* - `workspaceSummary` — first ~200 chars of `.pugi/PUGI.md` if the
|
|
15
|
+
* repo has one, else the directory basename.
|
|
16
16
|
*
|
|
17
17
|
* The helper is pure-ish (reads the filesystem but does not mutate it)
|
|
18
18
|
* so the production caller in `runtime/cli.ts` can call it eagerly at
|
|
@@ -27,16 +27,26 @@
|
|
|
27
27
|
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
28
28
|
import { basename, resolve as resolvePath } from 'node:path';
|
|
29
29
|
import { slugForCwd } from './history.js';
|
|
30
|
+
import { isBareMode } from '../bare-mode/index.js';
|
|
31
|
+
/**
|
|
32
|
+
* Workspace summary shown when the operator launched with `--bare` (or
|
|
33
|
+
* `PUGI_BARE=1`). bare mode disables project auto-discovery
|
|
34
|
+
* across the CLI, so we never read `.pugi/PUGI.md` and never advertise
|
|
35
|
+
* a real workspace label to admin-api. Explicit string so the splash +
|
|
36
|
+
* status bar agree, and so operators triaging "why is Pugi ignoring
|
|
37
|
+
* my repo" see a clear cause.
|
|
38
|
+
*/
|
|
39
|
+
export const BARE_MODE_WORKSPACE_LABEL = '(bare mode - auto-discovery disabled)';
|
|
30
40
|
/** Cap on the PUGI.md head we forward. Mirrors the admin-api clamp. */
|
|
31
41
|
const PUGI_MD_HEAD_LIMIT = 200;
|
|
32
42
|
/**
|
|
33
43
|
* Workspace label shown when the cwd has no project markers (no .git,
|
|
34
|
-
* no package.json, no PUGI.md). Per CEO
|
|
44
|
+
* no package.json, no PUGI.md). Per CEO dogfood, the
|
|
35
45
|
* previous behaviour leaked the parent directory basename (e.g.
|
|
36
46
|
* `codeforge-io`) into the splash as if it were a real workspace,
|
|
37
|
-
* confusing
|
|
47
|
+
* confusing Pugi/Pugi about what repo she was looking at. The
|
|
38
48
|
* unbound label is a single explicit string so the splash + status bar
|
|
39
|
-
* read the same warning.
|
|
49
|
+
* read the same warning.
|
|
40
50
|
*/
|
|
41
51
|
export const UNBOUND_WORKSPACE_LABEL = '(not bound - run /init OR cd into project)';
|
|
42
52
|
/**
|
|
@@ -48,9 +58,21 @@ export const UNBOUND_WORKSPACE_LABEL = '(not bound - run /init OR cd into projec
|
|
|
48
58
|
export function resolveWorkspaceContext(cwd) {
|
|
49
59
|
const normalised = resolvePath(cwd);
|
|
50
60
|
const slug = slugForCwd(normalised);
|
|
51
|
-
//
|
|
61
|
+
// `--bare` short-circuits BEFORE any PUGI.md
|
|
62
|
+
// / project-marker reads so the resolver never advertises a real
|
|
63
|
+
// workspace summary to admin-api. The cwd + slug still travel for
|
|
64
|
+
// telemetry, but the model + Pugi treat the session as if launched
|
|
65
|
+
// from a fresh, unbound directory.
|
|
66
|
+
if (isBareMode()) {
|
|
67
|
+
return {
|
|
68
|
+
workspaceCwd: normalised,
|
|
69
|
+
workspaceSlug: slug,
|
|
70
|
+
workspaceSummary: BARE_MODE_WORKSPACE_LABEL,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// wave 5: when the cwd has no project markers, prefer the
|
|
52
74
|
// explicit "not bound" summary so admin-api's prompt builder knows
|
|
53
|
-
// not to fabricate a workspace context for
|
|
75
|
+
// not to fabricate a workspace context for Pugi/Pugi. The cwd +
|
|
54
76
|
// slug still travel so the server can record where the operator
|
|
55
77
|
// launched from for telemetry, but the summary no longer leaks a
|
|
56
78
|
// stray parent-dir basename as if it were a real workspace.
|
|
@@ -69,18 +91,18 @@ export function resolveWorkspaceContext(cwd) {
|
|
|
69
91
|
* operator wandered into. The probe is intentionally cheap — three
|
|
70
92
|
* `existsSync` calls — and the order matches the brand convention:
|
|
71
93
|
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
94
|
+
* 1. `.git` — any clone of a real repo
|
|
95
|
+
* 2. `package.json` — JS/TS workspace root
|
|
96
|
+
* 3. `PUGI.md` — a Pugi-initialised workspace (root or
|
|
97
|
+
* `.pugi/PUGI.md`)
|
|
76
98
|
*
|
|
77
99
|
* Hitting any one of these counts the cwd as "bound" — the operator
|
|
78
100
|
* intentionally landed in a project. Hitting none means they ran
|
|
79
101
|
* `pugi` from `$HOME` or from the parent of a checkout; in that case
|
|
80
102
|
* the splash surfaces an explicit "not bound" label instead of leaking
|
|
81
103
|
* the parent-dir basename as if it were a workspace. The check
|
|
82
|
-
* mirrors the
|
|
83
|
-
* never fake-bind.
|
|
104
|
+
* mirrors the the upstream tool "no CLAUDE.md → silent context" rule —
|
|
105
|
+
* never fake-bind.
|
|
84
106
|
*/
|
|
85
107
|
export function isBoundWorkspace(cwd) {
|
|
86
108
|
const normalised = resolvePath(cwd);
|
|
@@ -105,7 +127,7 @@ export function isBoundWorkspace(cwd) {
|
|
|
105
127
|
* understands no real workspace was detected. The label is the only
|
|
106
128
|
* string the splash and status bar agree on, so we centralise the
|
|
107
129
|
* decision here instead of re-deriving in two places.
|
|
108
|
-
*
|
|
130
|
+
*
|
|
109
131
|
*/
|
|
110
132
|
export function resolveWorkspaceLabel(cwd) {
|
|
111
133
|
if (!isBoundWorkspace(cwd))
|
|
@@ -119,7 +141,7 @@ export function resolveWorkspaceLabel(cwd) {
|
|
|
119
141
|
/**
|
|
120
142
|
* Read the first ~200 chars of `.pugi/PUGI.md` if the file exists. The
|
|
121
143
|
* project's own description is the highest-signal one-line summary we
|
|
122
|
-
* can hand to
|
|
144
|
+
* can hand to Pugi — `pugi init` writes it on workspace creation, and
|
|
123
145
|
* the operator may have edited it since.
|
|
124
146
|
*
|
|
125
147
|
* Returns null on any FS error so the caller falls back to the
|
|
@@ -169,7 +191,7 @@ export function summariseMarkdown(raw) {
|
|
|
169
191
|
}
|
|
170
192
|
/**
|
|
171
193
|
* Drop a YAML front-matter block (`---\n…\n---`) from the head of a
|
|
172
|
-
* Markdown file.
|
|
194
|
+
* Markdown file. Pugi does not need to see the metadata; the prose body
|
|
173
195
|
* carries the project description.
|
|
174
196
|
*/
|
|
175
197
|
function stripFrontmatter(raw) {
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repo-map build orchestrator — .
|
|
3
|
+
*
|
|
4
|
+
* Single entry point that the CLI command + the engine boot path both
|
|
5
|
+
* call. Wires the scanner → extractor → cache → formatter pipeline
|
|
6
|
+
* together and surfaces a structured result the caller can render or
|
|
7
|
+
* inject without knowing the inner module shapes.
|
|
8
|
+
*
|
|
9
|
+
* The orchestrator is split out от cache.ts and the command handler
|
|
10
|
+
* so:
|
|
11
|
+
*
|
|
12
|
+
* 1. The CLI command + the engine system-prompt injector share one
|
|
13
|
+
* code path. Drift between the two would silently change what
|
|
14
|
+
* the model sees vs. what the operator sees.
|
|
15
|
+
*
|
|
16
|
+
* 2. The spec can exercise the full pipeline against a temp dir
|
|
17
|
+
* without mounting Ink or the engine.
|
|
18
|
+
*
|
|
19
|
+
* Pure-ish: reads from disk (the source files + the cache), but never
|
|
20
|
+
* mutates anything outside `.pugi/repo-map.json` and never logs. The
|
|
21
|
+
* caller decides whether к persist the cache (`writeCache: true`) or
|
|
22
|
+
* к compute the map в-memory (`writeCache: false` — useful for
|
|
23
|
+
* non-interactive `--json` invocations on read-only fs).
|
|
24
|
+
*/
|
|
25
|
+
import { readFileSync } from 'node:fs';
|
|
26
|
+
import { loadPugiIgnore } from '../context/pugiignore.js';
|
|
27
|
+
import { defaultCachePath, diffCacheAgainstScan, mergeCache, readRepoMapCache, writeRepoMapCache, } from './cache.js';
|
|
28
|
+
import { extractFromFile } from './extractor.js';
|
|
29
|
+
import { scanRepoForMap } from './scanner.js';
|
|
30
|
+
import { formatRepoMap } from './formatter.js';
|
|
31
|
+
/**
|
|
32
|
+
* Run the full pipeline. Returns a structured verdict; never throws.
|
|
33
|
+
* The 'too-large' branch fires when the workspace exceeds the file
|
|
34
|
+
* cap — callers surface a hint к the operator ("repo too large for
|
|
35
|
+
* inline map — try .pugiignore") and skip injection.
|
|
36
|
+
*/
|
|
37
|
+
export function buildRepoMap(options) {
|
|
38
|
+
const root = options.root;
|
|
39
|
+
const refresh = options.refresh === true;
|
|
40
|
+
const writeCache = options.writeCache !== false;
|
|
41
|
+
const cachePath = options.cachePath ?? defaultCachePath(root);
|
|
42
|
+
const readFile = options.readFile ?? ((path) => readFileSync(path, 'utf8'));
|
|
43
|
+
const ignore = loadPugiIgnore(root);
|
|
44
|
+
const scan = scanRepoForMap({ root, ignore });
|
|
45
|
+
if (!scan.ok) {
|
|
46
|
+
return {
|
|
47
|
+
ok: false,
|
|
48
|
+
root,
|
|
49
|
+
reason: scan.skipped.reason,
|
|
50
|
+
walked: scan.skipped.walked,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const prior = refresh ? null : readCacheOrNull(cachePath);
|
|
54
|
+
const diff = diffCacheAgainstScan(prior, scan.files);
|
|
55
|
+
const freshExtracts = new Map();
|
|
56
|
+
for (const file of diff.toRebuild) {
|
|
57
|
+
let source;
|
|
58
|
+
try {
|
|
59
|
+
source = readFile(file.absPath);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// File vanished или became unreadable mid-build — skip. The
|
|
63
|
+
// cache layer will just not have an entry for it; next refresh
|
|
64
|
+
// picks it up if it reappears.
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
freshExtracts.set(file.relPath, extractFromFile(file, source));
|
|
68
|
+
}
|
|
69
|
+
const cache = mergeCache({
|
|
70
|
+
root,
|
|
71
|
+
prior,
|
|
72
|
+
scanned: scan.files,
|
|
73
|
+
freshExtracts,
|
|
74
|
+
});
|
|
75
|
+
let cacheWritten = false;
|
|
76
|
+
if (writeCache) {
|
|
77
|
+
const writeResult = writeRepoMapCache(cachePath, cache);
|
|
78
|
+
cacheWritten = writeResult.ok;
|
|
79
|
+
}
|
|
80
|
+
// Surface the extracts в the same order the scanner produced (sorted
|
|
81
|
+
// by POSIX path) so callers iterating the result render deterministic
|
|
82
|
+
// output. The formatter does its own priority sort, so a different
|
|
83
|
+
// order here would only affect callers that iterate manually.
|
|
84
|
+
const extracts = [];
|
|
85
|
+
for (const file of scan.files) {
|
|
86
|
+
const entry = cache.entries[file.relPath];
|
|
87
|
+
if (entry)
|
|
88
|
+
extracts.push(entry.extract);
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
ok: true,
|
|
92
|
+
root,
|
|
93
|
+
cache,
|
|
94
|
+
extracts,
|
|
95
|
+
scanStats: scan.stats,
|
|
96
|
+
diffStats: {
|
|
97
|
+
rebuilt: diff.toRebuild.length,
|
|
98
|
+
reused: diff.reuse.length,
|
|
99
|
+
dropped: diff.toDrop.length,
|
|
100
|
+
},
|
|
101
|
+
cachePath,
|
|
102
|
+
cacheWritten,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Convenience wrapper: build + format в one call. The engine boot
|
|
107
|
+
* path uses this so it does not have к know the formatter shape.
|
|
108
|
+
*/
|
|
109
|
+
export function buildAndFormatRepoMap(options) {
|
|
110
|
+
const build = buildRepoMap(options);
|
|
111
|
+
if (!build.ok)
|
|
112
|
+
return { build };
|
|
113
|
+
const format = formatRepoMap(build.extracts, {
|
|
114
|
+
maxBytes: options.formatBytesCap,
|
|
115
|
+
omitHeader: options.omitHeader,
|
|
116
|
+
});
|
|
117
|
+
return { build, format };
|
|
118
|
+
}
|
|
119
|
+
function readCacheOrNull(path) {
|
|
120
|
+
const verdict = readRepoMapCache(path);
|
|
121
|
+
if (verdict.ok)
|
|
122
|
+
return verdict.cache;
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=build.js.map
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repo-map cache — .
|
|
3
|
+
*
|
|
4
|
+
* Persists the result of the scan + extract passes к
|
|
5
|
+
* `.pugi/repo-map.json` so subsequent boots reuse the symbol table
|
|
6
|
+
* without re-walking the workspace. The cache key is `(mtimeMs, size)`
|
|
7
|
+
* per file, matching the heuristic used by Node's own native
|
|
8
|
+
* `node:fs.statSync` cache and Git's index format. A file whose mtime
|
|
9
|
+
* AND size match the cached entry is presumed unchanged; otherwise the
|
|
10
|
+
* extractor re-runs against the fresh contents.
|
|
11
|
+
*
|
|
12
|
+
* Why not a content hash:
|
|
13
|
+
*
|
|
14
|
+
* The file-cache module already hashes by content for the
|
|
15
|
+
* working-set heuristic, and the L28 use case is different — repo-
|
|
16
|
+
* map invalidation is "should we re-parse" not "is this content
|
|
17
|
+
* identical to the last cached version". A content-hash sweep would
|
|
18
|
+
* re-read every source file on every boot, defeating the purpose of
|
|
19
|
+
* a cache. mtime + size matches the cost profile (one stat call per
|
|
20
|
+
* file, no read) while catching every realistic edit pattern
|
|
21
|
+
* (editors universally update mtime; even `truncate` updates size).
|
|
22
|
+
*
|
|
23
|
+
* Why JSON not SQLite:
|
|
24
|
+
*
|
|
25
|
+
* The index-store ships as a flat JSON blob and parses в <50 ms
|
|
26
|
+
* for typical repos (~2000 files); we match the format так the
|
|
27
|
+
* doctor probe + cabinet sync tools can read repo-map.json without
|
|
28
|
+
* spinning up a SQLite driver. The blob is gzip-friendly if a future
|
|
29
|
+
* sprint wants к ship it across the wire.
|
|
30
|
+
*
|
|
31
|
+
* Schema versioning: every cache entry carries `schemaVersion`. When
|
|
32
|
+
* the extractor surface changes (new symbol kinds, new summary
|
|
33
|
+
* format), bump the constant and existing caches are dropped on the
|
|
34
|
+
* next boot — same pattern as the migration runner.
|
|
35
|
+
*
|
|
36
|
+
* Pure-ish surface: reads / writes use `node:fs` sync, no logging.
|
|
37
|
+
* Errors are converted к structured results so the caller can decide
|
|
38
|
+
* whether к surface them or fall back к a cold rebuild.
|
|
39
|
+
*/
|
|
40
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
41
|
+
import { dirname, join } from 'node:path';
|
|
42
|
+
/**
|
|
43
|
+
* Cache format version. Bump when:
|
|
44
|
+
* - `RepoMapSymbol` adds / renames a field
|
|
45
|
+
* - `RepoMapFileExtract` adds / renames a field
|
|
46
|
+
* - The mtime + size invalidation contract changes
|
|
47
|
+
*
|
|
48
|
+
* Old caches with a mismatched version are dropped on read.
|
|
49
|
+
*/
|
|
50
|
+
export const REPO_MAP_CACHE_VERSION = 1;
|
|
51
|
+
/**
|
|
52
|
+
* Default location for the workspace cache file. Mirrors the rest of
|
|
53
|
+
* the `.pugi/` convention: `<workspace>/.pugi/repo-map.json`.
|
|
54
|
+
*/
|
|
55
|
+
export function defaultCachePath(workspaceRoot) {
|
|
56
|
+
return join(workspaceRoot, '.pugi', 'repo-map.json');
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Read the cache file from disk. Returns a structured verdict; never
|
|
60
|
+
* throws. The 'missing' branch is the cold-boot happy path. The
|
|
61
|
+
* 'parse-error' branch signals corruption — the caller drops the
|
|
62
|
+
* cache and rebuilds. The 'version-mismatch' branch fires after an
|
|
63
|
+
* extractor schema bump.
|
|
64
|
+
*/
|
|
65
|
+
export function readRepoMapCache(path) {
|
|
66
|
+
if (!existsSync(path)) {
|
|
67
|
+
return { ok: false, reason: 'missing' };
|
|
68
|
+
}
|
|
69
|
+
let raw;
|
|
70
|
+
try {
|
|
71
|
+
raw = readFileSync(path, 'utf8');
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return { ok: false, reason: 'parse-error' };
|
|
75
|
+
}
|
|
76
|
+
let parsed;
|
|
77
|
+
try {
|
|
78
|
+
parsed = JSON.parse(raw);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return { ok: false, reason: 'parse-error' };
|
|
82
|
+
}
|
|
83
|
+
if (!isCacheShape(parsed)) {
|
|
84
|
+
return { ok: false, reason: 'parse-error' };
|
|
85
|
+
}
|
|
86
|
+
if (parsed.schemaVersion !== REPO_MAP_CACHE_VERSION) {
|
|
87
|
+
return { ok: false, reason: 'version-mismatch' };
|
|
88
|
+
}
|
|
89
|
+
return { ok: true, cache: parsed };
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Write the cache atomically (write-then-rename) so a concurrent
|
|
93
|
+
* reader never sees a half-flushed JSON blob. Errors are surfaced as
|
|
94
|
+
* a structured boolean so the caller can decide whether к escalate —
|
|
95
|
+
* the engine boot path silently swallows write failures because
|
|
96
|
+
* repo-map is a best-effort enrichment.
|
|
97
|
+
*/
|
|
98
|
+
export function writeRepoMapCache(path, cache) {
|
|
99
|
+
try {
|
|
100
|
+
const dir = dirname(path);
|
|
101
|
+
if (!existsSync(dir)) {
|
|
102
|
+
mkdirSync(dir, { recursive: true });
|
|
103
|
+
}
|
|
104
|
+
const body = JSON.stringify(cache, null, 2) + '\n';
|
|
105
|
+
const tmp = path + '.tmp';
|
|
106
|
+
writeFileSync(tmp, body, { encoding: 'utf8' });
|
|
107
|
+
// Atomic rename. `fs.renameSync` is atomic on POSIX + on NTFS when
|
|
108
|
+
// src + dst live on the same volume, which is always true for
|
|
109
|
+
// `.pugi/`-local writes.
|
|
110
|
+
renameSync(tmp, path);
|
|
111
|
+
return { ok: true };
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
return {
|
|
115
|
+
ok: false,
|
|
116
|
+
error: error instanceof Error ? error.message : String(error),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
export function diffCacheAgainstScan(prior, scanned) {
|
|
121
|
+
const toRebuild = [];
|
|
122
|
+
const reuse = [];
|
|
123
|
+
const seen = new Set();
|
|
124
|
+
for (const file of scanned) {
|
|
125
|
+
seen.add(file.relPath);
|
|
126
|
+
const entry = prior?.entries[file.relPath];
|
|
127
|
+
if (!entry
|
|
128
|
+
|| entry.mtimeMs !== file.mtimeMs
|
|
129
|
+
|| entry.sizeBytes !== file.sizeBytes) {
|
|
130
|
+
toRebuild.push(file);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
reuse.push(file.relPath);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const toDrop = [];
|
|
137
|
+
if (prior) {
|
|
138
|
+
for (const key of Object.keys(prior.entries)) {
|
|
139
|
+
if (!seen.has(key))
|
|
140
|
+
toDrop.push(key);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return { toRebuild, toDrop, reuse };
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Stitch a fresh cache object together from the prior surviving
|
|
147
|
+
* entries + the newly-extracted ones. Pure helper — the caller is
|
|
148
|
+
* responsible for actually writing the result.
|
|
149
|
+
*/
|
|
150
|
+
export function mergeCache(args) {
|
|
151
|
+
const { root, prior, scanned, freshExtracts } = args;
|
|
152
|
+
const entries = {};
|
|
153
|
+
for (const file of scanned) {
|
|
154
|
+
const fresh = freshExtracts.get(file.relPath);
|
|
155
|
+
if (fresh) {
|
|
156
|
+
entries[file.relPath] = {
|
|
157
|
+
mtimeMs: file.mtimeMs,
|
|
158
|
+
sizeBytes: file.sizeBytes,
|
|
159
|
+
extract: fresh,
|
|
160
|
+
};
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const priorEntry = prior?.entries[file.relPath];
|
|
164
|
+
if (priorEntry) {
|
|
165
|
+
entries[file.relPath] = priorEntry;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
schemaVersion: REPO_MAP_CACHE_VERSION,
|
|
170
|
+
root,
|
|
171
|
+
builtAtMs: args.nowMs ?? Date.now(),
|
|
172
|
+
entries,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function isCacheShape(value) {
|
|
176
|
+
if (typeof value !== 'object' || value === null)
|
|
177
|
+
return false;
|
|
178
|
+
const v = value;
|
|
179
|
+
return (typeof v.schemaVersion === 'number'
|
|
180
|
+
&& typeof v.root === 'string'
|
|
181
|
+
&& typeof v.builtAtMs === 'number'
|
|
182
|
+
&& typeof v.entries === 'object'
|
|
183
|
+
&& v.entries !== null);
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=cache.js.map
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maximum symbols carried per file. The formatter further truncates к
|
|
3
|
+
* the 2 KB injection budget, but capping per-file here keeps a single
|
|
4
|
+
* giant `index.ts` from monopolising the map.
|
|
5
|
+
*/
|
|
6
|
+
export const MAX_SYMBOLS_PER_FILE = 50;
|
|
7
|
+
/**
|
|
8
|
+
* Extract symbols + summary from a single file. `kind` is dispatched
|
|
9
|
+
* on the lowercased extension. Files with an unrecognised extension
|
|
10
|
+
* return an empty symbol list with `summary: null` — the scanner
|
|
11
|
+
* already filtered by `SUPPORTED_EXTENSIONS` so this branch is mostly
|
|
12
|
+
* defensive (test fixtures sometimes pass `.txt`).
|
|
13
|
+
*/
|
|
14
|
+
export function extractFromFile(file, source) {
|
|
15
|
+
switch (file.ext) {
|
|
16
|
+
case '.ts':
|
|
17
|
+
case '.tsx':
|
|
18
|
+
case '.js':
|
|
19
|
+
case '.jsx':
|
|
20
|
+
case '.mjs':
|
|
21
|
+
case '.cjs':
|
|
22
|
+
return extractFromTsLike(file, source);
|
|
23
|
+
case '.md':
|
|
24
|
+
case '.mdx':
|
|
25
|
+
return extractFromMarkdown(file, source);
|
|
26
|
+
default:
|
|
27
|
+
return {
|
|
28
|
+
relPath: file.relPath,
|
|
29
|
+
ext: file.ext,
|
|
30
|
+
summary: null,
|
|
31
|
+
symbols: [],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/* ------------------------- TS / JS extraction ------------------------- */
|
|
36
|
+
/**
|
|
37
|
+
* Identifier pattern. We use the ASCII subset (letters/digits/`$`/`_`)
|
|
38
|
+
* rather than the full unicode ID start/continue range because the
|
|
39
|
+
* unicode tables would inflate the bundle by ~50 KB for zero benefit
|
|
40
|
+
* — Pugi's customers are typing English identifiers. Unicode names
|
|
41
|
+
* in source still PARSE (they just do not surface в the repo-map);
|
|
42
|
+
* the formatter degrades gracefully.
|
|
43
|
+
*/
|
|
44
|
+
const IDENT = '[A-Za-z_$][A-Za-z0-9_$]*';
|
|
45
|
+
/**
|
|
46
|
+
* Top-level declaration shapes:
|
|
47
|
+
*
|
|
48
|
+
* export? (default)? class Foo { ... }
|
|
49
|
+
* export? (default)? function foo() { ... }
|
|
50
|
+
* export? (default)? async function foo() { ... }
|
|
51
|
+
* export? (const|let|var) foo = (args) => { ... }
|
|
52
|
+
* export? (const|let|var) foo = async (args) => { ... }
|
|
53
|
+
* export? (const|let|var) foo = function (args) { ... }
|
|
54
|
+
* export? interface Foo { ... }
|
|
55
|
+
* export? type Foo = ...
|
|
56
|
+
* export? enum Foo { ... }
|
|
57
|
+
*
|
|
58
|
+
* The patterns anchor on start-of-line (`^` with the `m` flag) so they
|
|
59
|
+
* never match nested declarations inside a class body or a closure.
|
|
60
|
+
* That intentionally loses precision for module-level IIFEs (e.g.
|
|
61
|
+
* `;(function init() {})()`), but the L28 budget already drops nested
|
|
62
|
+
* symbols, so the loss is invisible to the operator.
|
|
63
|
+
*/
|
|
64
|
+
const TS_CLASS_RE = new RegExp(`^(export\\s+(?:default\\s+)?(?:abstract\\s+)?)?class\\s+(${IDENT})`, 'gm');
|
|
65
|
+
const TS_FUNCTION_RE = new RegExp(`^(export\\s+(?:default\\s+)?)?(?:async\\s+)?function\\s*\\*?\\s+(${IDENT})`, 'gm');
|
|
66
|
+
/**
|
|
67
|
+
* Arrow / function-expression `const|let|var foo = (...) => ...` shape.
|
|
68
|
+
* The optional type annotation between the identifier and the `=` is
|
|
69
|
+
* non-trivial because TS allows `(...) => ...` IN the annotation
|
|
70
|
+
* itself ("export const x: () => number = () => 1"). We allow `=>`
|
|
71
|
+
* as a unit inside the annotation by matching the char class
|
|
72
|
+
* `(?:=>|[^=\\n])*?` which consumes either an arrow token OR a
|
|
73
|
+
* non-`=` char; the trailing assignment `=` is then the first
|
|
74
|
+
* standalone `=` (`(?!>)`) on the line. The match tail (`=>` arrow,
|
|
75
|
+
* parenthesised arg list, generic, or `function` keyword) anchors
|
|
76
|
+
* the RHS so plain `const x = 1` does not surface as a function.
|
|
77
|
+
*/
|
|
78
|
+
const TS_ARROW_RE = new RegExp(`^(export\\s+)?(?:const|let|var)\\s+(${IDENT})\\b(?:=>|[^=\\n])*?=(?!>)\\s*(?:async\\s*)?(?:\\(|<|function\\b)`, 'gm');
|
|
79
|
+
const TS_INTERFACE_RE = new RegExp(`^(export\\s+)?interface\\s+(${IDENT})`, 'gm');
|
|
80
|
+
const TS_TYPE_RE = new RegExp(`^(export\\s+)?type\\s+(${IDENT})\\s*[=<]`, 'gm');
|
|
81
|
+
const TS_ENUM_RE = new RegExp(`^(export\\s+)?(?:const\\s+)?enum\\s+(${IDENT})`, 'gm');
|
|
82
|
+
/**
|
|
83
|
+
* Lead JSDoc / TSDoc block: `/** ... */` at the start of the file or
|
|
84
|
+
* preceded only by whitespace + import statements. We pick the first
|
|
85
|
+
* non-empty narrative line — the convention across Pugi's own codebase
|
|
86
|
+
* is that the headline sentence sits at the top of the block.
|
|
87
|
+
*/
|
|
88
|
+
const LEAD_DOC_RE = /\/\*\*([\s\S]*?)\*\//;
|
|
89
|
+
function extractFromTsLike(file, source) {
|
|
90
|
+
const symbols = [];
|
|
91
|
+
// We compute line numbers lazily by counting newlines в `source`
|
|
92
|
+
// up к each match's `.index`. Building a single prefix-newline
|
|
93
|
+
// array once is cheaper than calling `source.slice(0, idx).split`
|
|
94
|
+
// per match.
|
|
95
|
+
const lineStarts = computeLineStarts(source);
|
|
96
|
+
const lineFor = (offset) => binarySearchLine(lineStarts, offset);
|
|
97
|
+
const pushMatches = (regex, kind, exportIndex, nameIndex) => {
|
|
98
|
+
regex.lastIndex = 0;
|
|
99
|
+
let match;
|
|
100
|
+
while ((match = regex.exec(source)) !== null) {
|
|
101
|
+
if (symbols.length >= MAX_SYMBOLS_PER_FILE)
|
|
102
|
+
return;
|
|
103
|
+
const name = match[nameIndex];
|
|
104
|
+
if (!name)
|
|
105
|
+
continue;
|
|
106
|
+
const exported = Boolean(match[exportIndex]);
|
|
107
|
+
symbols.push({
|
|
108
|
+
name,
|
|
109
|
+
kind,
|
|
110
|
+
exported,
|
|
111
|
+
line: lineFor(match.index),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
pushMatches(TS_CLASS_RE, 'class', 1, 2);
|
|
116
|
+
pushMatches(TS_FUNCTION_RE, 'function', 1, 2);
|
|
117
|
+
pushMatches(TS_ARROW_RE, 'const', 1, 2);
|
|
118
|
+
pushMatches(TS_INTERFACE_RE, 'interface', 1, 2);
|
|
119
|
+
pushMatches(TS_TYPE_RE, 'type', 1, 2);
|
|
120
|
+
pushMatches(TS_ENUM_RE, 'enum', 1, 2);
|
|
121
|
+
// Dedupe by `name + kind` — `export const x = function x() {}` would
|
|
122
|
+
// otherwise show up twice (arrow regex + function regex). Keep the
|
|
123
|
+
// earlier one (lowest line) and the exported flag if either match
|
|
124
|
+
// saw it.
|
|
125
|
+
const dedup = new Map();
|
|
126
|
+
for (const sym of symbols) {
|
|
127
|
+
const key = `${sym.kind}::${sym.name}`;
|
|
128
|
+
const prior = dedup.get(key);
|
|
129
|
+
if (!prior) {
|
|
130
|
+
dedup.set(key, sym);
|
|
131
|
+
}
|
|
132
|
+
else if (sym.line < prior.line) {
|
|
133
|
+
dedup.set(key, { ...sym, exported: prior.exported || sym.exported });
|
|
134
|
+
}
|
|
135
|
+
else if (sym.exported && !prior.exported) {
|
|
136
|
+
dedup.set(key, { ...prior, exported: true });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const deduped = Array.from(dedup.values()).sort((a, b) => a.line - b.line);
|
|
140
|
+
return {
|
|
141
|
+
relPath: file.relPath,
|
|
142
|
+
ext: file.ext,
|
|
143
|
+
summary: extractLeadDocSummary(source),
|
|
144
|
+
symbols: deduped.slice(0, MAX_SYMBOLS_PER_FILE),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Extract the first narrative sentence from a leading JSDoc block.
|
|
149
|
+
* Returns null when no block is present in the first 4 KB of the
|
|
150
|
+
* file (cap protects against huge generated headers).
|
|
151
|
+
*/
|
|
152
|
+
export function extractLeadDocSummary(source) {
|
|
153
|
+
const window = source.slice(0, 4096);
|
|
154
|
+
const match = LEAD_DOC_RE.exec(window);
|
|
155
|
+
if (!match)
|
|
156
|
+
return null;
|
|
157
|
+
const body = match[1] ?? '';
|
|
158
|
+
for (const rawLine of body.split('\n')) {
|
|
159
|
+
const line = rawLine.replace(/^\s*\*\s?/u, '').trim();
|
|
160
|
+
if (line.length === 0)
|
|
161
|
+
continue;
|
|
162
|
+
// Skip the `@param` / `@returns` / `@deprecated` block prefixes —
|
|
163
|
+
// the summary is the prose lead, not the tag soup.
|
|
164
|
+
if (line.startsWith('@'))
|
|
165
|
+
continue;
|
|
166
|
+
// Truncate at 120 chars so a 5-line philosophical preamble does
|
|
167
|
+
// not blow the formatter's column budget.
|
|
168
|
+
return line.length > 120 ? line.slice(0, 117) + '...' : line;
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
/* -------------------------- Markdown extraction -------------------------- */
|
|
173
|
+
const MD_HEADING_RE = /^(#{1,6})\s+(.+?)\s*#*\s*$/gm;
|
|
174
|
+
function extractFromMarkdown(file, source) {
|
|
175
|
+
const symbols = [];
|
|
176
|
+
const lineStarts = computeLineStarts(source);
|
|
177
|
+
MD_HEADING_RE.lastIndex = 0;
|
|
178
|
+
let match;
|
|
179
|
+
while ((match = MD_HEADING_RE.exec(source)) !== null) {
|
|
180
|
+
if (symbols.length >= MAX_SYMBOLS_PER_FILE)
|
|
181
|
+
break;
|
|
182
|
+
const level = match[1]?.length ?? 0;
|
|
183
|
+
// Only H1 + H2 surface — the L28 budget cannot afford H3+ depth
|
|
184
|
+
// and the operator-readable map is meant к answer "what is в
|
|
185
|
+
// this file" not "what is the full TOC".
|
|
186
|
+
if (level > 2)
|
|
187
|
+
continue;
|
|
188
|
+
const name = (match[2] ?? '').trim();
|
|
189
|
+
if (!name)
|
|
190
|
+
continue;
|
|
191
|
+
symbols.push({
|
|
192
|
+
name,
|
|
193
|
+
kind: 'heading',
|
|
194
|
+
exported: true,
|
|
195
|
+
line: binarySearchLine(lineStarts, match.index),
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
relPath: file.relPath,
|
|
200
|
+
ext: file.ext,
|
|
201
|
+
summary: extractMarkdownSummary(source),
|
|
202
|
+
symbols,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* First non-heading paragraph in the markdown file. Truncated к 120
|
|
207
|
+
* chars like the JSDoc summary so the formatter stays single-line.
|
|
208
|
+
*/
|
|
209
|
+
export function extractMarkdownSummary(source) {
|
|
210
|
+
const lines = source.split('\n').slice(0, 200);
|
|
211
|
+
let sawHeading = false;
|
|
212
|
+
for (const raw of lines) {
|
|
213
|
+
const line = raw.trim();
|
|
214
|
+
if (line.length === 0)
|
|
215
|
+
continue;
|
|
216
|
+
if (line.startsWith('#')) {
|
|
217
|
+
sawHeading = true;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
// Skip front-matter delimiters and HTML/markdown directives.
|
|
221
|
+
if (line === '---' || line.startsWith('<!--'))
|
|
222
|
+
continue;
|
|
223
|
+
if (!sawHeading) {
|
|
224
|
+
// Pre-heading body — usually front-matter content. Skip it; the
|
|
225
|
+
// first POST-heading paragraph is the operator-facing summary.
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
return line.length > 120 ? line.slice(0, 117) + '...' : line;
|
|
229
|
+
}
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
/* ----------------------------- helpers ----------------------------- */
|
|
233
|
+
function computeLineStarts(source) {
|
|
234
|
+
const starts = [0];
|
|
235
|
+
for (let i = 0; i < source.length; i += 1) {
|
|
236
|
+
if (source.charCodeAt(i) === 10 /* \n */)
|
|
237
|
+
starts.push(i + 1);
|
|
238
|
+
}
|
|
239
|
+
return starts;
|
|
240
|
+
}
|
|
241
|
+
function binarySearchLine(starts, offset) {
|
|
242
|
+
// Returns 1-based line number.
|
|
243
|
+
let lo = 0;
|
|
244
|
+
let hi = starts.length - 1;
|
|
245
|
+
while (lo < hi) {
|
|
246
|
+
const mid = (lo + hi + 1) >>> 1;
|
|
247
|
+
if (starts[mid] <= offset)
|
|
248
|
+
lo = mid;
|
|
249
|
+
else
|
|
250
|
+
hi = mid - 1;
|
|
251
|
+
}
|
|
252
|
+
return lo + 1;
|
|
253
|
+
}
|
|
254
|
+
//# sourceMappingURL=extractor.js.map
|