@geraldmaron/construct 1.0.0 → 1.0.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/.env.example +9 -6
- package/LICENSE +201 -21
- package/README.md +132 -257
- package/agents/contracts.json +387 -0
- package/agents/prompts/cx-accessibility.md +8 -0
- package/agents/prompts/cx-ai-engineer.md +92 -0
- package/agents/prompts/cx-architect.md +10 -2
- package/agents/prompts/cx-business-strategist.md +13 -0
- package/agents/prompts/cx-data-analyst.md +80 -0
- package/agents/prompts/cx-data-engineer.md +4 -0
- package/agents/prompts/cx-debugger.md +8 -0
- package/agents/prompts/cx-designer.md +19 -0
- package/agents/prompts/cx-devil-advocate.md +4 -0
- package/agents/prompts/cx-docs-keeper.md +128 -0
- package/agents/prompts/cx-engineer.md +13 -0
- package/agents/prompts/cx-evaluator.md +4 -0
- package/agents/prompts/cx-explorer.md +12 -0
- package/agents/prompts/cx-legal-compliance.md +13 -0
- package/agents/prompts/cx-operations.md +13 -0
- package/agents/prompts/cx-orchestrator.md +107 -4
- package/agents/prompts/cx-platform-engineer.md +75 -0
- package/agents/prompts/cx-product-manager.md +8 -0
- package/agents/prompts/cx-qa.md +100 -0
- package/agents/prompts/cx-rd-lead.md +13 -0
- package/agents/prompts/cx-release-manager.md +8 -0
- package/agents/prompts/cx-researcher.md +21 -1
- package/agents/prompts/cx-reviewer.md +8 -0
- package/agents/prompts/cx-security.md +104 -0
- package/agents/prompts/cx-sre.md +104 -0
- package/agents/prompts/cx-test-automation.md +4 -0
- package/agents/prompts/cx-trace-reviewer.md +7 -3
- package/agents/prompts/cx-ux-researcher.md +4 -0
- package/agents/registry.json +365 -90
- package/agents/role-manifests.json +217 -0
- package/bin/construct +3345 -141
- package/bin/construct-postinstall.mjs +87 -0
- package/commands/build/feature.md +6 -6
- package/commands/plan/feature.md +6 -5
- package/commands/plan/requirements.md +1 -1
- package/commands/ship/status.md +1 -1
- package/commands/work/drive.md +3 -3
- package/commands/work/optimize-prompts.md +3 -3
- package/db/{migrations → schema}/001_init.sql +2 -0
- package/db/schema/002_pgvector.sql +182 -0
- package/db/schema/003_intake.sql +47 -0
- package/examples/README.md +85 -0
- package/examples/internal/roles/architect/bad/clever-plan-without-contracts.md +30 -0
- package/examples/internal/roles/architect/golden/explicit-tradeoff-before-plan.md +23 -0
- package/examples/internal/roles/engineer/bad/speculative-abstraction.md +30 -0
- package/examples/internal/roles/engineer/golden/read-before-write.md +28 -0
- package/examples/internal/roles/orchestrator/bad/everything-becomes-multi-agent.md +29 -0
- package/examples/internal/roles/orchestrator/golden/minimal-dispatch.md +22 -0
- package/examples/internal/roles/qa/bad/coverage-theater.md +30 -0
- package/examples/internal/roles/qa/golden/regression-gate.md +23 -0
- package/examples/internal/roles/reviewer/bad/lgtm-without-verification.md +29 -0
- package/examples/internal/roles/reviewer/golden/find-structural-risk-first.md +28 -0
- package/examples/personas/construct/adversarial/ignore-instruction-to-skip-approval.md +22 -0
- package/examples/personas/construct/bad/commit-without-approval.md +30 -0
- package/examples/personas/construct/boundary/blocked-needs-main-input.md +29 -0
- package/examples/personas/construct/golden/branch-approval-before-mutation.md +36 -0
- package/examples/personas/construct/golden/focused-direct-answer.md +28 -0
- package/examples/provider-plugin/README.md +34 -0
- package/examples/provider-plugin/index.mjs +74 -0
- package/examples/provider-plugin/package.json +15 -0
- package/examples/seed-observations/README.md +38 -0
- package/examples/seed-observations/anti-patterns.md +44 -0
- package/examples/seed-observations/decisions.md +36 -0
- package/examples/seed-observations/patterns.md +42 -0
- package/lib/agent-contracts-enforce.mjs +158 -0
- package/lib/agent-contracts.mjs +231 -0
- package/lib/agents/postconditions.mjs +126 -0
- package/lib/agents/schema.mjs +124 -0
- package/lib/artifact-capture.mjs +183 -0
- package/lib/audit-trail.mjs +149 -0
- package/lib/auto-docs.mjs +245 -65
- package/lib/beads/auto-close.mjs +126 -0
- package/lib/beads/drift.mjs +171 -0
- package/lib/beads-automation.mjs +542 -0
- package/lib/beads-client.mjs +518 -0
- package/lib/beads-lock.mjs +377 -0
- package/lib/beads-optimistic.mjs +365 -0
- package/lib/bootstrap/built-ins.mjs +136 -0
- package/lib/bootstrap/lazy-install.mjs +161 -0
- package/lib/bootstrap/resources.mjs +120 -0
- package/lib/bootstrap.mjs +105 -0
- package/lib/cache-governor.js +213 -0
- package/lib/cache-strategy-anthropic.js +62 -0
- package/lib/cache-strategy-google.js +79 -0
- package/lib/cache-strategy-none.js +30 -0
- package/lib/cache-strategy-openai.js +48 -0
- package/lib/cache-strategy.js +91 -0
- package/lib/claude-allow.mjs +149 -0
- package/lib/cli-commands.mjs +413 -258
- package/lib/codex-config.mjs +3 -1
- package/lib/comment-lint.mjs +55 -6
- package/lib/completions.mjs +21 -6
- package/lib/config/alias.mjs +56 -0
- package/lib/config/project-config.mjs +335 -0
- package/lib/config/schema.mjs +159 -0
- package/lib/context-router.mjs +308 -0
- package/lib/cost-ledger.mjs +177 -0
- package/lib/cost.mjs +171 -10
- package/lib/dashboard-static.mjs +158 -0
- package/lib/deployment-mode.mjs +86 -0
- package/lib/deprecate.mjs +49 -0
- package/lib/dispatch-batch.js +183 -0
- package/lib/distill.mjs +21 -8
- package/lib/doc-stamp.mjs +164 -0
- package/lib/doc-verify.mjs +119 -0
- package/lib/docs-routing.mjs +89 -0
- package/lib/docs-verify.mjs +417 -0
- package/lib/doctor/audit.mjs +71 -0
- package/lib/doctor/cli.mjs +99 -0
- package/lib/doctor/escalate.mjs +29 -0
- package/lib/doctor/index.mjs +140 -0
- package/lib/doctor/report.mjs +170 -0
- package/lib/doctor/watchers/bd-watch.mjs +117 -0
- package/lib/doctor/watchers/cost.mjs +130 -0
- package/lib/doctor/watchers/disk.mjs +122 -0
- package/lib/doctor/watchers/handoffs.mjs +33 -0
- package/lib/doctor/watchers/process-pressure.mjs +60 -0
- package/lib/doctor/watchers/service-health.mjs +188 -0
- package/lib/document-extract.mjs +288 -0
- package/lib/document-ingest.mjs +230 -0
- package/lib/drop.mjs +282 -0
- package/lib/embed/approval-queue.mjs +176 -0
- package/lib/embed/artifact.mjs +349 -0
- package/lib/embed/authority-guard.mjs +155 -0
- package/lib/embed/cli.mjs +408 -0
- package/lib/embed/config.mjs +355 -0
- package/lib/embed/conflict-detection.mjs +264 -0
- package/lib/embed/customer-profiles.mjs +480 -0
- package/lib/embed/daemon.mjs +1309 -0
- package/lib/embed/demand-fetch.mjs +449 -0
- package/lib/embed/docs-lifecycle.mjs +349 -0
- package/lib/embed/inbox-live-watcher.mjs +119 -0
- package/lib/embed/inbox.mjs +343 -0
- package/lib/embed/intake-metrics.mjs +190 -0
- package/lib/embed/jobs/vector-sync.mjs +198 -0
- package/lib/embed/notifications.mjs +75 -0
- package/lib/embed/output.mjs +79 -0
- package/lib/embed/providers/github.mjs +295 -0
- package/lib/embed/providers/jira.mjs +192 -0
- package/lib/embed/providers/linear.mjs +186 -0
- package/lib/embed/providers/registry.mjs +115 -0
- package/lib/embed/providers/slack.mjs +203 -0
- package/lib/embed/recommendation-store.mjs +378 -0
- package/lib/embed/roadmap.mjs +374 -0
- package/lib/embed/role-framing.mjs +110 -0
- package/lib/embed/scheduler.mjs +99 -0
- package/lib/embed/semantic.mjs +325 -0
- package/lib/embed/snapshot.mjs +191 -0
- package/lib/embed/supervision.mjs +235 -0
- package/lib/embed/target-resolver.mjs +186 -0
- package/lib/embed/worker.mjs +62 -0
- package/lib/embed/workspaces.mjs +297 -0
- package/lib/engine/chunker-headings.mjs +110 -0
- package/lib/engine/compressor-heuristic.mjs +100 -0
- package/lib/engine/consolidate.mjs +287 -0
- package/lib/engine/contracts.mjs +126 -0
- package/lib/engine/defaults.mjs +129 -0
- package/lib/engine/eval-retrieval.mjs +148 -0
- package/lib/engine/fuser-rrf.mjs +62 -0
- package/lib/engine/index.mjs +37 -0
- package/lib/engine/registry.mjs +146 -0
- package/lib/engine/reranker-mmr.mjs +90 -0
- package/lib/engine/tokens.mjs +77 -0
- package/lib/entity-store.mjs +280 -0
- package/lib/env-config.mjs +69 -4
- package/lib/evals/retrieval-bench.mjs +159 -0
- package/lib/evaluator-optimizer.mjs +317 -0
- package/lib/features.mjs +159 -29
- package/lib/gates-audit.mjs +236 -0
- package/lib/git-hooks/prepare-commit-msg +58 -0
- package/lib/handoffs/cleanup.mjs +159 -0
- package/lib/handoffs/contract.mjs +162 -0
- package/lib/handoffs/inventory.mjs +120 -0
- package/lib/headhunt.mjs +28 -47
- package/lib/health-check.mjs +399 -0
- package/lib/hook-health.mjs +442 -0
- package/lib/hooks/_lib/log.mjs +82 -0
- package/lib/hooks/adaptive-lint.mjs +26 -2
- package/lib/hooks/agent-tracker.mjs +171 -14
- package/lib/hooks/audit-reads.mjs +109 -0
- package/lib/hooks/audit-trail.mjs +153 -0
- package/lib/hooks/bash-output-logger.mjs +71 -0
- package/lib/hooks/block-no-verify.mjs +41 -0
- package/lib/hooks/ci-status-check.mjs +82 -0
- package/lib/hooks/comment-lint.mjs +29 -7
- package/lib/hooks/config-protection.mjs +39 -18
- package/lib/hooks/context-watch.mjs +137 -0
- package/lib/hooks/context-window-recovery.mjs +4 -20
- package/lib/hooks/dep-audit.mjs +16 -0
- package/lib/hooks/doc-coupling-check.mjs +80 -0
- package/lib/hooks/edit-accumulator.mjs +3 -0
- package/lib/hooks/edit-error-recovery.mjs +3 -0
- package/lib/hooks/edit-guard.mjs +45 -4
- package/lib/hooks/env-check.mjs +3 -0
- package/lib/hooks/guard-bash.mjs +58 -1
- package/lib/hooks/mcp-audit.mjs +18 -20
- package/lib/hooks/mcp-health-check.mjs +36 -0
- package/lib/hooks/model-fallback.mjs +42 -56
- package/lib/hooks/policy-engine.mjs +209 -0
- package/lib/hooks/post-merge-docs-check.mjs +63 -0
- package/lib/hooks/pre-compact.mjs +4 -24
- package/lib/hooks/pre-push-gate.mjs +200 -37
- package/lib/hooks/proactive-activation.mjs +284 -0
- package/lib/hooks/read-tracker.mjs +27 -4
- package/lib/hooks/readme-age-check.mjs +77 -0
- package/lib/hooks/registry-sync.mjs +12 -5
- package/lib/hooks/scan-secrets.mjs +50 -7
- package/lib/hooks/session-optimize.mjs +311 -0
- package/lib/hooks/session-start.mjs +287 -28
- package/lib/hooks/stop-notify.mjs +236 -96
- package/lib/hooks/stop-typecheck.mjs +13 -1
- package/lib/hooks/test-watch.mjs +68 -0
- package/lib/host-capabilities.mjs +31 -10
- package/lib/init-docs.mjs +731 -291
- package/lib/init-unified.mjs +1000 -0
- package/lib/init-update.mjs +168 -0
- package/lib/init.mjs +107 -0
- package/lib/install/first-invocation.mjs +119 -0
- package/lib/install/stage-project.mjs +69 -0
- package/lib/intake/classify.mjs +278 -0
- package/lib/intake/feedback.mjs +273 -0
- package/lib/intake/filesystem-queue.mjs +158 -0
- package/lib/intake/intake-config.mjs +132 -0
- package/lib/intake/postgres-queue.mjs +198 -0
- package/lib/intake/prepare.mjs +139 -0
- package/lib/intake/queue.mjs +83 -0
- package/lib/intake/session-prelude.mjs +50 -0
- package/lib/integrations/intake-integrations.mjs +740 -0
- package/lib/intent-classifier.mjs +253 -0
- package/lib/knowledge/layout.mjs +72 -0
- package/lib/knowledge/rag.mjs +331 -0
- package/lib/knowledge/search.mjs +315 -0
- package/lib/knowledge/trends.mjs +261 -0
- package/lib/logger.mjs +85 -0
- package/lib/mcp/broker.mjs +124 -0
- package/lib/mcp/server.mjs +554 -854
- package/lib/mcp/tools/document.mjs +132 -0
- package/lib/mcp/tools/memory.mjs +209 -0
- package/lib/mcp/tools/project.mjs +349 -0
- package/lib/mcp/tools/skills.mjs +409 -0
- package/lib/mcp/tools/storage.mjs +60 -0
- package/lib/mcp/tools/telemetry.mjs +315 -0
- package/lib/mcp/tools/workflow.mjs +112 -0
- package/lib/mcp-catalog.json +54 -3
- package/lib/mcp-manager.mjs +96 -48
- package/lib/mcp-platform-config.mjs +22 -22
- package/lib/memory-stats.mjs +121 -0
- package/lib/mode-commands.mjs +124 -0
- package/lib/model-free-selector.mjs +184 -0
- package/lib/model-pricing.mjs +152 -0
- package/lib/model-registry.mjs +226 -0
- package/lib/model-router.mjs +362 -386
- package/lib/observation-store.mjs +391 -0
- package/lib/ollama-manager.mjs +428 -0
- package/lib/opencode-config.mjs +13 -3
- package/lib/opencode-runtime-plugin.mjs +70 -38
- package/lib/opencode-telemetry.mjs +64 -6
- package/lib/orchestration-policy.mjs +509 -6
- package/lib/overrides/resolver.mjs +207 -0
- package/lib/parity.mjs +147 -0
- package/lib/paths.mjs +33 -0
- package/lib/performance/generate.mjs +212 -0
- package/lib/plugin-registry.mjs +268 -0
- package/lib/policy/engine.mjs +130 -0
- package/lib/policy/unified-gates.mjs +96 -0
- package/lib/project-detection.mjs +129 -0
- package/lib/project-init-shared.mjs +272 -0
- package/lib/project-profile.mjs +447 -0
- package/lib/prompt-composer.js +434 -0
- package/lib/prompt-metadata.mjs +1 -1
- package/lib/provider-capabilities-anthropic.js +44 -0
- package/lib/provider-capabilities-deepseek.js +37 -0
- package/lib/provider-capabilities-generic.js +26 -0
- package/lib/provider-capabilities-google.js +47 -0
- package/lib/provider-capabilities-openai.js +45 -0
- package/lib/provider-capabilities.js +142 -0
- package/lib/providers/atlassian-confluence/index.mjs +103 -0
- package/lib/providers/atlassian-jira/index.mjs +100 -0
- package/lib/providers/auth-manager.mjs +126 -0
- package/lib/providers/circuit-breaker.mjs +124 -0
- package/lib/providers/contract.mjs +93 -0
- package/lib/providers/github/index.mjs +126 -0
- package/lib/providers/registry.mjs +184 -0
- package/lib/providers/salesforce/index.mjs +100 -0
- package/lib/providers/slack/index.mjs +80 -0
- package/lib/reflect.mjs +137 -0
- package/lib/research-lint.mjs +164 -0
- package/lib/resources/budget.mjs +259 -0
- package/lib/role-preload.mjs +21 -6
- package/lib/roles/approval-surface.mjs +54 -0
- package/lib/roles/cli.mjs +118 -0
- package/lib/roles/event-bus.mjs +79 -0
- package/lib/roles/fence.mjs +84 -0
- package/lib/roles/gateway.mjs +260 -0
- package/lib/roles/hook-emit.mjs +37 -0
- package/lib/roles/manifest.mjs +48 -0
- package/lib/roles/router.mjs +27 -0
- package/lib/runtime-pressure.mjs +360 -0
- package/lib/schema-artifact.mjs +134 -0
- package/lib/schema-infer.mjs +551 -0
- package/lib/server/auth.mjs +168 -0
- package/lib/server/chat.mjs +336 -0
- package/lib/server/cors.mjs +77 -0
- package/lib/server/csrf.mjs +91 -0
- package/lib/server/index.mjs +1927 -78
- package/lib/server/insights.mjs +765 -0
- package/lib/server/rate-limit.mjs +91 -0
- package/lib/server/static/assets/index-ab25c707.js +70 -0
- package/lib/server/static/assets/index-f0c80a2b.css +1 -0
- package/lib/server/static/index.html +12 -817
- package/lib/server/telemetry-login.mjs +108 -0
- package/lib/server/webhook.mjs +510 -0
- package/lib/service-manager.mjs +522 -58
- package/lib/services/pattern-promotion-service.mjs +167 -0
- package/lib/services/telemetry-backend.mjs +178 -0
- package/lib/session-store.mjs +374 -0
- package/lib/setup-prompts.mjs +96 -0
- package/lib/setup.mjs +523 -36
- package/lib/skills-apply.mjs +280 -0
- package/lib/skills-scope.mjs +118 -0
- package/lib/status.mjs +261 -70
- package/lib/storage/admin.mjs +355 -0
- package/lib/storage/backend.mjs +2 -1
- package/lib/storage/backup.mjs +347 -0
- package/lib/storage/embeddings-engine.mjs +133 -0
- package/lib/storage/embeddings-legacy.mjs +85 -0
- package/lib/storage/embeddings-local.mjs +108 -0
- package/lib/storage/embeddings-ollama.mjs +78 -0
- package/lib/storage/embeddings-openai.mjs +85 -0
- package/lib/storage/embeddings.mjs +92 -33
- package/lib/storage/file-lock.mjs +130 -0
- package/lib/storage/fusion.mjs +95 -0
- package/lib/storage/hybrid-query.mjs +34 -27
- package/lib/storage/migrations.mjs +187 -0
- package/lib/storage/postgres-backup.mjs +124 -0
- package/lib/storage/sql-store.mjs +5 -15
- package/lib/storage/state-source.mjs +12 -13
- package/lib/storage/store-version.mjs +115 -0
- package/lib/storage/sync.mjs +144 -35
- package/lib/storage/unified-storage.mjs +550 -0
- package/lib/storage/vector-client.mjs +286 -0
- package/lib/storage/vector-store.mjs +71 -30
- package/lib/task-graph/generate.mjs +135 -0
- package/lib/task-graph/schema.mjs +81 -0
- package/lib/task-graph/store.mjs +71 -0
- package/lib/telemetry/backends/local.mjs +62 -0
- package/lib/telemetry/backends/{langfuse.mjs → remote.mjs} +27 -14
- package/lib/telemetry/backfill.mjs +180 -0
- package/lib/telemetry/eval-datasets.mjs +203 -0
- package/lib/telemetry/{langfuse-ingest.mjs → ingest.mjs} +26 -20
- package/lib/telemetry/intent-verifications.mjs +86 -0
- package/lib/telemetry/llm-judge.mjs +350 -0
- package/lib/telemetry/model-pricing-catalog.mjs +557 -0
- package/lib/telemetry/setup.mjs +151 -0
- package/lib/telemetry/skill-calls.mjs +78 -0
- package/lib/telemetry/team-rollup.mjs +4 -4
- package/lib/token-engine.js +117 -0
- package/lib/token-estimator-anthropic.js +15 -0
- package/lib/token-estimator-deepseek.js +13 -0
- package/lib/token-estimator-default.js +13 -0
- package/lib/token-estimator-google.js +13 -0
- package/lib/token-estimator-openai.js +13 -0
- package/lib/toolkit-env.mjs +1 -1
- package/lib/tty-prompts.mjs +211 -0
- package/lib/uninstall/uninstall.mjs +423 -0
- package/lib/update.mjs +115 -0
- package/lib/upgrade.mjs +141 -0
- package/lib/validator.mjs +51 -2
- package/lib/validators/skills.mjs +142 -0
- package/lib/wireframe.mjs +422 -0
- package/lib/worker/entrypoint.mjs +241 -0
- package/lib/worker/evidence.mjs +107 -0
- package/lib/worker/run.mjs +154 -0
- package/lib/worker/trace.mjs +182 -0
- package/lib/workflow-state.mjs +14 -18
- package/package.json +21 -8
- package/personas/construct.md +53 -51
- package/platforms/claude/CLAUDE.md +1 -1
- package/platforms/claude/settings.template.json +115 -68
- package/platforms/opencode/config.template.json +3 -7
- package/rules/common/agents.md +11 -38
- package/rules/common/beads-hygiene.md +75 -0
- package/rules/common/code-review.md +11 -100
- package/rules/common/coding-style.md +5 -3
- package/rules/common/comments.md +30 -111
- package/rules/common/commit-approval.md +53 -0
- package/rules/common/cx-agent-routing.md +1 -0
- package/rules/common/development-workflow.md +23 -41
- package/rules/common/doc-ownership.md +54 -0
- package/rules/common/efficiency.md +57 -0
- package/rules/common/framing.md +76 -0
- package/rules/common/git-workflow.md +2 -4
- package/rules/common/patterns.md +3 -7
- package/rules/common/performance.md +15 -31
- package/rules/common/release-gates.md +69 -0
- package/rules/common/research.md +107 -0
- package/rules/common/security.md +6 -6
- package/rules/common/skill-composition.md +67 -0
- package/rules/common/testing.md +15 -27
- package/rules/golang/hooks.md +0 -4
- package/rules/policy/bootstrap.yaml +11 -0
- package/rules/policy/drive.yaml +8 -0
- package/rules/policy/task.yaml +9 -0
- package/rules/policy/workflow.yaml +9 -0
- package/rules/python/hooks.md +0 -4
- package/rules/swift/hooks.md +0 -4
- package/rules/typescript/hooks.md +0 -4
- package/rules/web/hooks.md +0 -2
- package/{sync-agents.mjs → scripts/sync-agents.mjs} +306 -61
- package/skills/ai/prompt-optimizer.md +7 -7
- package/skills/compliance/ai-disclosure.md +58 -0
- package/skills/compliance/data-privacy.md +46 -0
- package/skills/compliance/license-audit.md +40 -0
- package/skills/compliance/regulatory-review.md +61 -0
- package/skills/docs/document-ingest-workflow.md +52 -0
- package/skills/docs/evidence-ingest-workflow.md +9 -0
- package/skills/docs/init-docs.md +51 -18
- package/skills/docs/product-intelligence-workflow.md +12 -0
- package/skills/docs/product-signal-workflow.md +4 -0
- package/skills/docs/research-workflow.md +23 -8
- package/skills/docs/runbook-workflow.md +1 -1
- package/skills/operating/orchestration-reference.md +151 -0
- package/skills/roles/architect.md +5 -0
- package/skills/roles/engineer.md +32 -0
- package/skills/roles/operator.md +12 -0
- package/skills/roles/reviewer.md +23 -0
- package/skills/routing.md +1 -1
- package/templates/devcontainer/Dockerfile.devcontainer +38 -0
- package/templates/devcontainer/devcontainer.json +31 -0
- package/templates/distribution/bootstrap.ps1 +142 -0
- package/templates/distribution/bootstrap.sh +196 -0
- package/templates/distribution/run.mjs +187 -0
- package/templates/docs/adr.md +44 -8
- package/templates/docs/changelog-entry.md +43 -0
- package/templates/docs/construct_guide.md +149 -0
- package/templates/docs/evidence-brief.md +2 -2
- package/templates/docs/meta-prd.md +140 -19
- package/templates/docs/onboarding.md +57 -0
- package/templates/docs/prd.md +159 -15
- package/templates/docs/research-brief.md +4 -4
- package/templates/homebrew/construct.rb +67 -0
- package/langfuse/docker-compose.yml +0 -82
- package/lib/eval-harness.mjs +0 -59
- package/lib/hooks/bootstrap-guard.mjs +0 -90
- package/lib/hooks/console-warn.mjs +0 -43
- package/lib/hooks/continuation-enforcer.mjs +0 -72
- package/lib/hooks/drive-guard.mjs +0 -89
- package/lib/hooks/mcp-task-scope.mjs +0 -47
- package/lib/hooks/task-completed-guard.mjs +0 -43
- package/lib/hooks/teammate-idle-guard.mjs +0 -54
- package/lib/hooks/workflow-guard.mjs +0 -62
- package/lib/prompt-composer.mjs +0 -196
- package/lib/review.mjs +0 -429
- package/lib/server/static/app.js +0 -841
- package/lib/telemetry/langfuse-model-sync.mjs +0 -270
- package/rules/common/hooks.md +0 -35
- package/rules/zh/README.md +0 -113
- package/rules/zh/agents.md +0 -55
- package/rules/zh/code-review.md +0 -129
- package/rules/zh/coding-style.md +0 -53
- package/rules/zh/development-workflow.md +0 -49
- package/rules/zh/git-workflow.md +0 -29
- package/rules/zh/hooks.md +0 -35
- package/rules/zh/patterns.md +0 -36
- package/rules/zh/performance.md +0 -60
- package/rules/zh/security.md +0 -34
- package/rules/zh/testing.md +0 -34
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/embed/workspaces.mjs — Multi-PM workspace management.
|
|
3
|
+
*
|
|
4
|
+
* Workspaces isolate PM workstreams while enabling cross-workspace visibility.
|
|
5
|
+
* Each workspace has an owner (PM), assigned customers, and product areas.
|
|
6
|
+
*
|
|
7
|
+
* Storage:
|
|
8
|
+
* ~/.cx/product-intel/workspaces/<workspace-id>.json
|
|
9
|
+
* ~/.cx/product-intel/workspaces/index.json — quick lookup
|
|
10
|
+
*
|
|
11
|
+
* Intake packets are routed to workspaces based on customer assignment
|
|
12
|
+
* or product area matching. Cross-workspace signals trigger notifications.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from 'node:fs';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { randomUUID } from 'node:crypto';
|
|
18
|
+
import { homedir } from 'node:os';
|
|
19
|
+
|
|
20
|
+
const WORKSPACES_DIR = join(homedir(), '.cx', 'product-intel', 'workspaces');
|
|
21
|
+
const INDEX_FILE = join(WORKSPACES_DIR, 'index.json');
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Ensure workspaces directory exists.
|
|
25
|
+
*/
|
|
26
|
+
function ensureDir() {
|
|
27
|
+
if (!existsSync(WORKSPACES_DIR)) {
|
|
28
|
+
mkdirSync(WORKSPACES_DIR, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Read the index file.
|
|
34
|
+
* @returns {{ [key: string]: { id: string, name: string, owner: string, status: string } }}
|
|
35
|
+
*/
|
|
36
|
+
function readIndex() {
|
|
37
|
+
if (!existsSync(INDEX_FILE)) return {};
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(readFileSync(INDEX_FILE, 'utf8'));
|
|
40
|
+
} catch (err) {
|
|
41
|
+
process.stderr.write('[workspaces.mjs] readIndex: ' + (err?.message ?? String(err)) + '\n');
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Write the index file.
|
|
48
|
+
* @param {object} index
|
|
49
|
+
*/
|
|
50
|
+
function writeIndex(index) {
|
|
51
|
+
ensureDir();
|
|
52
|
+
writeFileSync(INDEX_FILE, JSON.stringify(index, null, 2) + '\n');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Create a new workspace.
|
|
57
|
+
*
|
|
58
|
+
* @param {object} opts
|
|
59
|
+
* @param {string} opts.name - Workspace name (required)
|
|
60
|
+
* @param {string} opts.owner - PM or owner name (required)
|
|
61
|
+
* @param {string} [opts.description] - Workspace description
|
|
62
|
+
* @param {string[]} [opts.productAreas] - Product areas this workspace covers
|
|
63
|
+
* @param {string[]} [opts.customerIds] - Customer IDs assigned to this workspace
|
|
64
|
+
* @returns {{ id: string, workspace: Workspace }}
|
|
65
|
+
*/
|
|
66
|
+
export function createWorkspace({ name, owner, description, productAreas = [], customerIds = [] }) {
|
|
67
|
+
if (!name || typeof name !== 'string' || !name.trim()) {
|
|
68
|
+
throw new Error('name is required');
|
|
69
|
+
}
|
|
70
|
+
if (!owner || typeof owner !== 'string' || !owner.trim()) {
|
|
71
|
+
throw new Error('owner is required');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
ensureDir();
|
|
75
|
+
|
|
76
|
+
const id = `ws-${randomUUID().slice(0, 8)}`;
|
|
77
|
+
const workspace = {
|
|
78
|
+
id,
|
|
79
|
+
name: name.trim(),
|
|
80
|
+
owner,
|
|
81
|
+
description: description || '',
|
|
82
|
+
productAreas,
|
|
83
|
+
customerIds,
|
|
84
|
+
status: 'active',
|
|
85
|
+
createdAt: new Date().toISOString(),
|
|
86
|
+
updatedAt: new Date().toISOString(),
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const filePath = join(WORKSPACES_DIR, `${id}.json`);
|
|
90
|
+
writeFileSync(filePath, JSON.stringify(workspace, null, 2) + '\n');
|
|
91
|
+
|
|
92
|
+
// Update index
|
|
93
|
+
const index = readIndex();
|
|
94
|
+
index[id] = {
|
|
95
|
+
id,
|
|
96
|
+
name: workspace.name,
|
|
97
|
+
owner: workspace.owner,
|
|
98
|
+
status: workspace.status,
|
|
99
|
+
};
|
|
100
|
+
writeIndex(index);
|
|
101
|
+
|
|
102
|
+
return { id, workspace };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get a workspace by ID.
|
|
107
|
+
* @param {string} workspaceId
|
|
108
|
+
* @returns {Workspace | null}
|
|
109
|
+
*/
|
|
110
|
+
export function getWorkspace(workspaceId) {
|
|
111
|
+
ensureDir();
|
|
112
|
+
|
|
113
|
+
const filePath = join(WORKSPACES_DIR, `${workspaceId}.json`);
|
|
114
|
+
if (!existsSync(filePath)) return null;
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
return JSON.parse(readFileSync(filePath, 'utf8'));
|
|
118
|
+
} catch (err) {
|
|
119
|
+
process.stderr.write('[workspaces.mjs] getWorkspace: ' + (err?.message ?? String(err)) + '\n');
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* List all workspaces.
|
|
126
|
+
* @param {object} [opts]
|
|
127
|
+
* @param {string} [opts.owner] - Filter by owner
|
|
128
|
+
* @param {string} [opts.status] - Filter by status
|
|
129
|
+
* @returns {Array<Workspace>}
|
|
130
|
+
*/
|
|
131
|
+
export function listWorkspaces(opts = {}) {
|
|
132
|
+
ensureDir();
|
|
133
|
+
|
|
134
|
+
const index = readIndex();
|
|
135
|
+
let results = Object.values(index).map(entry => getWorkspace(entry.id)).filter(Boolean);
|
|
136
|
+
|
|
137
|
+
if (opts.owner) {
|
|
138
|
+
results = results.filter(r => r.owner === opts.owner);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (opts.status) {
|
|
142
|
+
results = results.filter(r => r.status === opts.status);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return results.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Update a workspace.
|
|
150
|
+
*
|
|
151
|
+
* @param {string} workspaceId
|
|
152
|
+
* @param {object} updates
|
|
153
|
+
* @param {string} [updates.name]
|
|
154
|
+
* @param {string} [updates.owner]
|
|
155
|
+
* @param {string} [updates.description]
|
|
156
|
+
* @param {string[]} [updates.productAreas]
|
|
157
|
+
* @param {string[]} [updates.customerIds]
|
|
158
|
+
* @param {string} [updates.status]
|
|
159
|
+
* @returns {{ success: boolean, updatedAt: string }}
|
|
160
|
+
*/
|
|
161
|
+
export function updateWorkspace(workspaceId, updates) {
|
|
162
|
+
const workspace = getWorkspace(workspaceId);
|
|
163
|
+
if (!workspace) {
|
|
164
|
+
throw new Error(`Workspace not found: ${workspaceId}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const updated = {
|
|
168
|
+
...workspace,
|
|
169
|
+
...updates,
|
|
170
|
+
updatedAt: new Date().toISOString(),
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const filePath = join(WORKSPACES_DIR, `${workspaceId}.json`);
|
|
174
|
+
writeFileSync(filePath, JSON.stringify(updated, null, 2) + '\n');
|
|
175
|
+
|
|
176
|
+
// Update index
|
|
177
|
+
const index = readIndex();
|
|
178
|
+
if (index[workspaceId]) {
|
|
179
|
+
index[workspaceId] = {
|
|
180
|
+
id: updated.id,
|
|
181
|
+
name: updated.name,
|
|
182
|
+
owner: updated.owner,
|
|
183
|
+
status: updated.status,
|
|
184
|
+
};
|
|
185
|
+
writeIndex(index);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return { success: true, updatedAt: updated.updatedAt };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Delete a workspace.
|
|
193
|
+
* @param {string} workspaceId
|
|
194
|
+
* @returns {{ success: boolean }}
|
|
195
|
+
*/
|
|
196
|
+
export function deleteWorkspace(workspaceId) {
|
|
197
|
+
const filePath = join(WORKSPACES_DIR, `${workspaceId}.json`);
|
|
198
|
+
if (!existsSync(filePath)) {
|
|
199
|
+
throw new Error(`Workspace not found: ${workspaceId}`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Remove from filesystem
|
|
203
|
+
const { unlinkSync } = require('node:fs');
|
|
204
|
+
unlinkSync(filePath);
|
|
205
|
+
|
|
206
|
+
// Remove from index
|
|
207
|
+
const index = readIndex();
|
|
208
|
+
delete index[workspaceId];
|
|
209
|
+
writeIndex(index);
|
|
210
|
+
|
|
211
|
+
return { success: true };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Route an intake signal to a workspace based on customer or product area.
|
|
216
|
+
*
|
|
217
|
+
* @param {object} signal
|
|
218
|
+
* @param {string} [signal.customerId] - Customer ID if known
|
|
219
|
+
* @param {string} [signal.productArea] - Product area mentioned
|
|
220
|
+
* @param {string} [signal.text] - Signal text for keyword matching
|
|
221
|
+
* @returns {{ workspaceId: string | null, workspaceName: string | null, isCrossWorkspace: boolean, matchedWorkspaces: string[] }}
|
|
222
|
+
*/
|
|
223
|
+
export function routeSignalToWorkspace(signal) {
|
|
224
|
+
const workspaces = listWorkspaces({ status: 'active' });
|
|
225
|
+
const matchedWorkspaces = [];
|
|
226
|
+
|
|
227
|
+
// If customer ID is provided, find workspace that owns this customer
|
|
228
|
+
if (signal.customerId) {
|
|
229
|
+
for (const ws of workspaces) {
|
|
230
|
+
if (ws.customerIds?.includes(signal.customerId)) {
|
|
231
|
+
matchedWorkspaces.push(ws.id);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// If product area is provided, match workspaces that cover it
|
|
237
|
+
if (signal.productArea) {
|
|
238
|
+
for (const ws of workspaces) {
|
|
239
|
+
if (ws.productAreas?.some(area =>
|
|
240
|
+
area.toLowerCase().includes(signal.productArea.toLowerCase())
|
|
241
|
+
)) {
|
|
242
|
+
if (!matchedWorkspaces.includes(ws.id)) {
|
|
243
|
+
matchedWorkspaces.push(ws.id);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// If text is provided, do keyword matching on product areas
|
|
250
|
+
if (signal.text && matchedWorkspaces.length === 0) {
|
|
251
|
+
const textLower = signal.text.toLowerCase();
|
|
252
|
+
for (const ws of workspaces) {
|
|
253
|
+
if (ws.productAreas?.some(area => textLower.includes(area.toLowerCase()))) {
|
|
254
|
+
matchedWorkspaces.push(ws.id);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Determine routing
|
|
260
|
+
if (matchedWorkspaces.length === 0) {
|
|
261
|
+
// No match — route to default workspace or null
|
|
262
|
+
const defaultWs = workspaces.find(ws => ws.name === 'default') || workspaces[0];
|
|
263
|
+
return {
|
|
264
|
+
workspaceId: defaultWs?.id || null,
|
|
265
|
+
workspaceName: defaultWs?.name || null,
|
|
266
|
+
isCrossWorkspace: false,
|
|
267
|
+
matchedWorkspaces: [],
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (matchedWorkspaces.length === 1) {
|
|
272
|
+
return {
|
|
273
|
+
workspaceId: matchedWorkspaces[0],
|
|
274
|
+
workspaceName: getWorkspace(matchedWorkspaces[0])?.name || null,
|
|
275
|
+
isCrossWorkspace: false,
|
|
276
|
+
matchedWorkspaces,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Multiple matches — this is a cross-workspace signal
|
|
281
|
+
return {
|
|
282
|
+
workspaceId: matchedWorkspaces[0], // Route to first match
|
|
283
|
+
workspaceName: getWorkspace(matchedWorkspaces[0])?.name || null,
|
|
284
|
+
isCrossWorkspace: true,
|
|
285
|
+
matchedWorkspaces,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Get workspace by owner name.
|
|
291
|
+
* @param {string} ownerName
|
|
292
|
+
* @returns {Workspace | null}
|
|
293
|
+
*/
|
|
294
|
+
export function getWorkspaceByOwner(ownerName) {
|
|
295
|
+
const workspaces = listWorkspaces({ status: 'active' });
|
|
296
|
+
return workspaces.find(ws => ws.owner === ownerName) || null;
|
|
297
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/engine/chunker-headings.mjs — heading-prefix Chunker plugin.
|
|
3
|
+
*
|
|
4
|
+
* Splits a markdown document into chunks at heading boundaries (`#`, `##`,
|
|
5
|
+
* `###`, …) and prepends each chunk with the chain of ancestor headings so
|
|
6
|
+
* that each retrieved chunk carries enough doc-relative context to be
|
|
7
|
+
* understood on its own. This is a zero-dep approximation of contextual
|
|
8
|
+
* retrieval: instead of a per-chunk LM-generated context (the strongest
|
|
9
|
+
* available technique), the heading chain provides "where in the document
|
|
10
|
+
* is this from" context for free.
|
|
11
|
+
*
|
|
12
|
+
* Plain-text docs (no headings) collapse to a single chunk; the input is
|
|
13
|
+
* returned as one chunk with no prefix.
|
|
14
|
+
*
|
|
15
|
+
* Plugin contract (Chunker):
|
|
16
|
+
* meta: { id }
|
|
17
|
+
* chunk(doc) → chunk[]
|
|
18
|
+
*
|
|
19
|
+
* `doc` shape:
|
|
20
|
+
* { id, title?, body, metadata? } — body is the markdown source.
|
|
21
|
+
*
|
|
22
|
+
* Output chunk shape:
|
|
23
|
+
* { id, body, title, prefix?, metadata }
|
|
24
|
+
*
|
|
25
|
+
* The prefix is the joined heading chain (e.g. "# Auth > ## JWT > ### Refresh").
|
|
26
|
+
* Callers that embed `prefix + body` will retrieve more accurately than if
|
|
27
|
+
* they embedded body alone.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const HEADING = /^(#{1,6})\s+(.+?)\s*$/;
|
|
31
|
+
|
|
32
|
+
function* iterChunks(body) {
|
|
33
|
+
const lines = String(body || '').split(/\r?\n/);
|
|
34
|
+
const stack = [];
|
|
35
|
+
let buffer = [];
|
|
36
|
+
let active = null;
|
|
37
|
+
|
|
38
|
+
function flush() {
|
|
39
|
+
const content = buffer.join('\n').trim();
|
|
40
|
+
if (content || active) {
|
|
41
|
+
yieldOne({
|
|
42
|
+
prefix: stack.map((h) => '#'.repeat(h.level) + ' ' + h.text).join(' > '),
|
|
43
|
+
title: active?.text || '',
|
|
44
|
+
body: content,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let queue = [];
|
|
50
|
+
function yieldOne(chunk) { queue.push(chunk); }
|
|
51
|
+
|
|
52
|
+
for (const line of lines) {
|
|
53
|
+
const m = line.match(HEADING);
|
|
54
|
+
if (m) {
|
|
55
|
+
flush();
|
|
56
|
+
buffer = [];
|
|
57
|
+
const level = m[1].length;
|
|
58
|
+
const text = m[2];
|
|
59
|
+
while (stack.length > 0 && stack[stack.length - 1].level >= level) {
|
|
60
|
+
stack.pop();
|
|
61
|
+
}
|
|
62
|
+
const heading = { level, text };
|
|
63
|
+
stack.push(heading);
|
|
64
|
+
active = heading;
|
|
65
|
+
while (queue.length) yield queue.shift();
|
|
66
|
+
} else {
|
|
67
|
+
buffer.push(line);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
flush();
|
|
71
|
+
while (queue.length) yield queue.shift();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function create(/* opts */) {
|
|
75
|
+
return {
|
|
76
|
+
meta: { id: 'headings-prefix' },
|
|
77
|
+
async chunk(doc) {
|
|
78
|
+
const docs = Array.isArray(doc) ? doc : [doc];
|
|
79
|
+
const out = [];
|
|
80
|
+
for (const d of docs) {
|
|
81
|
+
if (!d) continue;
|
|
82
|
+
const baseId = d.id || '';
|
|
83
|
+
const body = d.body || '';
|
|
84
|
+
const sub = [...iterChunks(body)];
|
|
85
|
+
if (sub.length <= 1) {
|
|
86
|
+
out.push({
|
|
87
|
+
id: baseId,
|
|
88
|
+
title: d.title || sub[0]?.title || '',
|
|
89
|
+
body,
|
|
90
|
+
prefix: sub[0]?.prefix || '',
|
|
91
|
+
metadata: d.metadata || {},
|
|
92
|
+
});
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
sub.forEach((chunk, i) => {
|
|
96
|
+
out.push({
|
|
97
|
+
id: `${baseId}#${i}`,
|
|
98
|
+
title: chunk.title,
|
|
99
|
+
body: chunk.body,
|
|
100
|
+
prefix: chunk.prefix,
|
|
101
|
+
metadata: { ...(d.metadata || {}), parentDocId: baseId, headingChainIndex: i },
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export default create;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/engine/compressor-heuristic.mjs — TF-IDF Compressor plugin (zero deps).
|
|
3
|
+
*
|
|
4
|
+
* Scores every sentence in the input by the mean IDF of its content terms
|
|
5
|
+
* (over the input itself), keeps the top-N sentences in their original order
|
|
6
|
+
* until the requested compression ratio is met, and returns the result. This
|
|
7
|
+
* is a lossy summarizer — fine for context blocks where the model needs the
|
|
8
|
+
* gist, not exact wording.
|
|
9
|
+
*
|
|
10
|
+
* The wider plan calls for a learned compressor (LLMLingua-2 token
|
|
11
|
+
* classification on ONNX) as a plugin override. That belongs in its own
|
|
12
|
+
* module so operators can opt into the model download. The default keeps
|
|
13
|
+
* the contract live, runs on every machine, and gives a measurable win
|
|
14
|
+
* for context-block compression where exact phrasing is not load-bearing.
|
|
15
|
+
*
|
|
16
|
+
* Plugin contract (Compressor):
|
|
17
|
+
* meta: { id, ratio }
|
|
18
|
+
* compress(text, opts) → text
|
|
19
|
+
*
|
|
20
|
+
* `ratio` is the target output / input length (0.5 = halve the text).
|
|
21
|
+
* `maxTokens` (estimated from text length) acts as a hard ceiling that
|
|
22
|
+
* overrides ratio when present.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { tokenize, buildTermFrequencies, buildIdf } from '../storage/embeddings.mjs';
|
|
26
|
+
|
|
27
|
+
const SENTENCE_BOUNDARY = /(?<=[.!?])\s+(?=[A-Z(\[])|\n{2,}/;
|
|
28
|
+
|
|
29
|
+
function splitSentences(text) {
|
|
30
|
+
const trimmed = String(text || '').trim();
|
|
31
|
+
if (!trimmed) return [];
|
|
32
|
+
return trimmed.split(SENTENCE_BOUNDARY).map((s) => s.trim()).filter(Boolean);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function scoreSentence(sentence, idf) {
|
|
36
|
+
const terms = tokenize(sentence);
|
|
37
|
+
if (terms.length === 0) return 0;
|
|
38
|
+
let total = 0;
|
|
39
|
+
let counted = 0;
|
|
40
|
+
for (const term of terms) {
|
|
41
|
+
const v = idf.get(term);
|
|
42
|
+
if (v !== undefined) {
|
|
43
|
+
total += v;
|
|
44
|
+
counted += 1;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return counted === 0 ? 0 : total / counted;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const APPROX_CHARS_PER_TOKEN = 4;
|
|
51
|
+
|
|
52
|
+
export function create({ ratio = 0.5 } = {}) {
|
|
53
|
+
return {
|
|
54
|
+
meta: { id: 'heuristic-tfidf-sentence', ratio },
|
|
55
|
+
async compress(text, opts = {}) {
|
|
56
|
+
const ratioEff = Math.max(0.05, Math.min(1, opts.ratio ?? ratio));
|
|
57
|
+
const input = String(text || '');
|
|
58
|
+
if (!input.trim()) return '';
|
|
59
|
+
|
|
60
|
+
const sentences = splitSentences(input);
|
|
61
|
+
if (sentences.length <= 1) return input;
|
|
62
|
+
|
|
63
|
+
const tfMaps = sentences.map((s) => buildTermFrequencies(s));
|
|
64
|
+
const idf = buildIdf(tfMaps);
|
|
65
|
+
|
|
66
|
+
const scored = sentences.map((sentence, index) => ({
|
|
67
|
+
index,
|
|
68
|
+
sentence,
|
|
69
|
+
score: scoreSentence(sentence, idf),
|
|
70
|
+
len: sentence.length,
|
|
71
|
+
}));
|
|
72
|
+
|
|
73
|
+
let targetChars = Math.floor(input.length * ratioEff);
|
|
74
|
+
if (typeof opts.maxTokens === 'number' && opts.maxTokens > 0) {
|
|
75
|
+
const ceiling = opts.maxTokens * APPROX_CHARS_PER_TOKEN;
|
|
76
|
+
if (ceiling < targetChars) targetChars = ceiling;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const ordered = [...scored].sort((a, b) => b.score - a.score || a.index - b.index);
|
|
80
|
+
const kept = new Set();
|
|
81
|
+
let used = 0;
|
|
82
|
+
for (const candidate of ordered) {
|
|
83
|
+
if (used + candidate.len > targetChars && kept.size > 0) continue;
|
|
84
|
+
kept.add(candidate.index);
|
|
85
|
+
used += candidate.len + 1;
|
|
86
|
+
if (used >= targetChars) break;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (kept.size === 0) kept.add(ordered[0].index);
|
|
90
|
+
|
|
91
|
+
return scored
|
|
92
|
+
.filter((s) => kept.has(s.index))
|
|
93
|
+
.sort((a, b) => a.index - b.index)
|
|
94
|
+
.map((s) => s.sentence)
|
|
95
|
+
.join(' ');
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export default create;
|