@stravica-ai/digital-operator-pa 0.1.0
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/LICENSE +21 -0
- package/README.md +170 -0
- package/bin/desk +9 -0
- package/bin/distill +2 -0
- package/bin/estate-sync +2 -0
- package/bin/launch +2 -0
- package/bin/memory-sync +2 -0
- package/bin/migrate-inspect +2 -0
- package/bin/pa +2 -0
- package/bin/pa-coordinator-hook +2 -0
- package/bin/pa-init +2 -0
- package/bin/research +2 -0
- package/bin/scheduler +2 -0
- package/bin/secrets +2 -0
- package/bin/slice +2 -0
- package/bin/smoke +9 -0
- package/bin/sync +2 -0
- package/dist/engineering-pa-cli/README.md +205 -0
- package/dist/engineering-pa-cli/bin/estate-sync +2 -0
- package/dist/engineering-pa-cli/bin/launch +2 -0
- package/dist/engineering-pa-cli/bin/pa +2 -0
- package/dist/engineering-pa-cli/bin/pa-coordinator-hook +12 -0
- package/dist/engineering-pa-cli/bin/research +2 -0
- package/dist/engineering-pa-cli/bin/scheduler +2 -0
- package/dist/engineering-pa-cli/bin/secrets +2 -0
- package/dist/engineering-pa-cli/bin/smoke +9 -0
- package/dist/engineering-pa-cli/bin/sync +2 -0
- package/dist/engineering-pa-cli/config/coordinator-allowlist.json +118 -0
- package/dist/engineering-pa-cli/docs/coordinator-hook.md +211 -0
- package/dist/engineering-pa-cli/docs/estate-sync.md +412 -0
- package/dist/engineering-pa-cli/docs/gsc.md +175 -0
- package/dist/engineering-pa-cli/docs/lint-linkedin.md +139 -0
- package/dist/engineering-pa-cli/docs/secrets.md +221 -0
- package/dist/engineering-pa-cli/docs/smoke.md +113 -0
- package/dist/engineering-pa-cli/docs/staging-alias.md +114 -0
- package/dist/engineering-pa-cli/index.mjs +17 -0
- package/dist/engineering-pa-cli/package.json +37 -0
- package/dist/engineering-pa-cli/src/autonomy/cli.mjs +329 -0
- package/dist/engineering-pa-cli/src/autonomy/envelope.mjs +301 -0
- package/dist/engineering-pa-cli/src/coordinator-hook/cli.mjs +140 -0
- package/dist/engineering-pa-cli/src/coordinator-hook/config.mjs +134 -0
- package/dist/engineering-pa-cli/src/coordinator-hook/message.mjs +47 -0
- package/dist/engineering-pa-cli/src/coordinator-hook/policy.mjs +211 -0
- package/dist/engineering-pa-cli/src/coordinator-hook/repo-root.mjs +44 -0
- package/dist/engineering-pa-cli/src/coordinator-hook/self.mjs +30 -0
- package/dist/engineering-pa-cli/src/environment/cli.mjs +287 -0
- package/dist/engineering-pa-cli/src/environment/probes.mjs +302 -0
- package/dist/engineering-pa-cli/src/gsc/auth.mjs +148 -0
- package/dist/engineering-pa-cli/src/gsc/cli.mjs +93 -0
- package/dist/engineering-pa-cli/src/gsc/cloudflare.mjs +318 -0
- package/dist/engineering-pa-cli/src/gsc/commands/add-property.mjs +70 -0
- package/dist/engineering-pa-cli/src/gsc/commands/list-properties.mjs +33 -0
- package/dist/engineering-pa-cli/src/gsc/commands/pull-analytics.mjs +130 -0
- package/dist/engineering-pa-cli/src/gsc/commands/submit-sitemap.mjs +41 -0
- package/dist/engineering-pa-cli/src/gsc/commands/verify-via-cf-dns.mjs +189 -0
- package/dist/engineering-pa-cli/src/knowledge/call-log.mjs +59 -0
- package/dist/engineering-pa-cli/src/knowledge/cli.mjs +242 -0
- package/dist/engineering-pa-cli/src/knowledge/eval.mjs +446 -0
- package/dist/engineering-pa-cli/src/knowledge/formatters.mjs +331 -0
- package/dist/engineering-pa-cli/src/knowledge/graph.mjs +710 -0
- package/dist/engineering-pa-cli/src/launch/README.md +214 -0
- package/dist/engineering-pa-cli/src/launch/cli.mjs +51 -0
- package/dist/engineering-pa-cli/src/launch/commands/launch.mjs +106 -0
- package/dist/engineering-pa-cli/src/launch/identity.mjs +43 -0
- package/dist/engineering-pa-cli/src/launch/planner.mjs +44 -0
- package/dist/engineering-pa-cli/src/launch/repo-root.mjs +53 -0
- package/dist/engineering-pa-cli/src/launch/runner.mjs +147 -0
- package/dist/engineering-pa-cli/src/launch/task-loader.mjs +85 -0
- package/dist/engineering-pa-cli/src/lint/cli.mjs +61 -0
- package/dist/engineering-pa-cli/src/lint/commands/linkedin.mjs +133 -0
- package/dist/engineering-pa-cli/src/lint/detectors.mjs +90 -0
- package/dist/engineering-pa-cli/src/lint/linter.mjs +69 -0
- package/dist/engineering-pa-cli/src/lint/reporter.mjs +32 -0
- package/dist/engineering-pa-cli/src/lint/rules.mjs +132 -0
- package/dist/engineering-pa-cli/src/pa/cli.mjs +369 -0
- package/dist/engineering-pa-cli/src/pa/doc-groups.mjs +164 -0
- package/dist/engineering-pa-cli/src/pa/io.mjs +80 -0
- package/dist/engineering-pa-cli/src/pa/project-cli.mjs +385 -0
- package/dist/engineering-pa-cli/src/pa/projects.mjs +352 -0
- package/dist/engineering-pa-cli/src/pa/repo-root.mjs +64 -0
- package/dist/engineering-pa-cli/src/pa/session.mjs +254 -0
- package/dist/engineering-pa-cli/src/pa/work-cli.mjs +164 -0
- package/dist/engineering-pa-cli/src/pa/work.mjs +196 -0
- package/dist/engineering-pa-cli/src/pa/workstreams.mjs +201 -0
- package/dist/engineering-pa-cli/src/research/README.md +360 -0
- package/dist/engineering-pa-cli/src/research/briefs/extractor.md +103 -0
- package/dist/engineering-pa-cli/src/research/briefs/searcher.md +113 -0
- package/dist/engineering-pa-cli/src/research/briefs/synthesizer.md +124 -0
- package/dist/engineering-pa-cli/src/research/briefs/verifier.md +86 -0
- package/dist/engineering-pa-cli/src/research/cli.mjs +141 -0
- package/dist/engineering-pa-cli/src/research/commands/gates.mjs +176 -0
- package/dist/engineering-pa-cli/src/research/commands/list.mjs +56 -0
- package/dist/engineering-pa-cli/src/research/commands/mark.mjs +97 -0
- package/dist/engineering-pa-cli/src/research/commands/paths.mjs +69 -0
- package/dist/engineering-pa-cli/src/research/commands/prune.mjs +86 -0
- package/dist/engineering-pa-cli/src/research/commands/run.mjs +94 -0
- package/dist/engineering-pa-cli/src/research/commands/show.mjs +43 -0
- package/dist/engineering-pa-cli/src/research/docs/operator-present-flow.md +120 -0
- package/dist/engineering-pa-cli/src/research/format.mjs +59 -0
- package/dist/engineering-pa-cli/src/research/promote.mjs +260 -0
- package/dist/engineering-pa-cli/src/research/schema/finding.schema.json +55 -0
- package/dist/engineering-pa-cli/src/research/schema/gates.schema.json +34 -0
- package/dist/engineering-pa-cli/src/research/schema/manifest.schema.json +59 -0
- package/dist/engineering-pa-cli/src/research/schema/plan.schema.json +41 -0
- package/dist/engineering-pa-cli/src/research/schema/sources.schema.json +31 -0
- package/dist/engineering-pa-cli/src/research/store/findings.mjs +61 -0
- package/dist/engineering-pa-cli/src/research/store/gates.mjs +73 -0
- package/dist/engineering-pa-cli/src/research/store/index.mjs +312 -0
- package/dist/engineering-pa-cli/src/research/store/manifest.mjs +56 -0
- package/dist/engineering-pa-cli/src/research/store/plan.mjs +59 -0
- package/dist/engineering-pa-cli/src/research/store/sources.mjs +44 -0
- package/dist/engineering-pa-cli/src/research/store/validate.mjs +92 -0
- package/dist/engineering-pa-cli/src/research/workflows/research-normal.js +573 -0
- package/dist/engineering-pa-cli/src/scheduler/cli.mjs +197 -0
- package/dist/engineering-pa-cli/src/secrets/cli.mjs +254 -0
- package/dist/engineering-pa-cli/src/secrets/commands/backup-template.mjs +67 -0
- package/dist/engineering-pa-cli/src/secrets/commands/backup.mjs +85 -0
- package/dist/engineering-pa-cli/src/secrets/commands/delete.mjs +58 -0
- package/dist/engineering-pa-cli/src/secrets/commands/find.mjs +36 -0
- package/dist/engineering-pa-cli/src/secrets/commands/get.mjs +49 -0
- package/dist/engineering-pa-cli/src/secrets/commands/init.mjs +69 -0
- package/dist/engineering-pa-cli/src/secrets/commands/list.mjs +37 -0
- package/dist/engineering-pa-cli/src/secrets/commands/rotate.mjs +46 -0
- package/dist/engineering-pa-cli/src/secrets/commands/set.mjs +123 -0
- package/dist/engineering-pa-cli/src/secrets/commands/status.mjs +146 -0
- package/dist/engineering-pa-cli/src/secrets/commands/vaults.mjs +57 -0
- package/dist/engineering-pa-cli/src/secrets/commands/verify.mjs +143 -0
- package/dist/engineering-pa-cli/src/secrets/vault.mjs +766 -0
- package/dist/engineering-pa-cli/src/smoke/checks.mjs +199 -0
- package/dist/engineering-pa-cli/src/smoke/cli.mjs +108 -0
- package/dist/engineering-pa-cli/src/smoke/commands/run.mjs +237 -0
- package/dist/engineering-pa-cli/src/smoke/runner.mjs +191 -0
- package/dist/engineering-pa-cli/src/staging-alias/cli.mjs +91 -0
- package/dist/engineering-pa-cli/src/staging-alias/cloudflare.mjs +380 -0
- package/dist/engineering-pa-cli/src/staging-alias/commands/list.mjs +36 -0
- package/dist/engineering-pa-cli/src/staging-alias/commands/set.mjs +188 -0
- package/dist/engineering-pa-cli/src/staging-alias/commands/status.mjs +113 -0
- package/dist/engineering-pa-cli/src/staging-alias/commands/unset.mjs +95 -0
- package/dist/engineering-pa-cli/src/staging-alias/sites.mjs +105 -0
- package/dist/engineering-pa-cli/src/sync/cli.mjs +151 -0
- package/dist/engineering-pa-cli/src/sync/git.mjs +149 -0
- package/dist/engineering-pa-cli/src/sync/lock.mjs +30 -0
- package/dist/engineering-pa-cli/src/sync/regenerated-artifacts.mjs +114 -0
- package/dist/engineering-pa-cli/src/sync/registry-scope.mjs +260 -0
- package/dist/engineering-pa-cli/src/sync/run.mjs +531 -0
- package/dist/engineering-pa-cli/src/sync/runner.mjs +62 -0
- package/dist/engineering-pa-cli/src/sync/runs-digest.mjs +138 -0
- package/dist/engineering-pa-core/README.md +60 -0
- package/dist/engineering-pa-core/bin/desk +9 -0
- package/dist/engineering-pa-core/bin/distill +2 -0
- package/dist/engineering-pa-core/bin/memory-sync +2 -0
- package/dist/engineering-pa-core/bin/slice +2 -0
- package/dist/engineering-pa-core/docs/autonomy-envelope-tracker.md +111 -0
- package/dist/engineering-pa-core/docs/blueprints/README.md +45 -0
- package/dist/engineering-pa-core/docs/blueprints/docker-services/blueprint.md +168 -0
- package/dist/engineering-pa-core/docs/blueprints/git-identity/blueprint.md +116 -0
- package/dist/engineering-pa-core/docs/blueprints/ideas/custom-workspace-views.md +9 -0
- package/dist/engineering-pa-core/docs/blueprints/ideas/knowledge-domain-patterns.md +15 -0
- package/dist/engineering-pa-core/docs/blueprints/ideas/office-integration.md +11 -0
- package/dist/engineering-pa-core/docs/blueprints/ideas/team-scaling.md +9 -0
- package/dist/engineering-pa-core/docs/blueprints/ideas/work-personal-separation.md +9 -0
- package/dist/engineering-pa-core/docs/blueprints/scheduled-distillation/blueprint.md +129 -0
- package/dist/engineering-pa-core/docs/blueprints/tool-integration/blueprint.md +110 -0
- package/dist/engineering-pa-core/docs/commands/init.md +159 -0
- package/dist/engineering-pa-core/docs/commands/schedule.md +98 -0
- package/dist/engineering-pa-core/docs/commands/upgrade.md +67 -0
- package/dist/engineering-pa-core/docs/entity-location-map.md +93 -0
- package/dist/engineering-pa-core/docs/knowledge-scaffold.md +121 -0
- package/dist/engineering-pa-core/docs/lock.md +155 -0
- package/dist/engineering-pa-core/docs/memory-sync.md +28 -0
- package/dist/engineering-pa-core/docs/pa-operating-model.md +457 -0
- package/dist/engineering-pa-core/docs/scheduling.md +35 -0
- package/dist/engineering-pa-core/docs/standards/README.md +24 -0
- package/dist/engineering-pa-core/docs/standards/briefing-contract.md +161 -0
- package/dist/engineering-pa-core/docs/standards/tooling.md +240 -0
- package/dist/engineering-pa-core/docs/tools/distill.md +230 -0
- package/dist/engineering-pa-core/docs/tools/memory-sync.md +98 -0
- package/dist/engineering-pa-core/docs/tools/scheduler.md +243 -0
- package/dist/engineering-pa-core/docs/tools/slice.md +150 -0
- package/dist/engineering-pa-core/docs/upstream-issue-filing.md +88 -0
- package/dist/engineering-pa-core/docs/working-directory.md +29 -0
- package/dist/engineering-pa-core/docs/workspace-operations.md +46 -0
- package/dist/engineering-pa-core/index.mjs +21 -0
- package/dist/engineering-pa-core/knowledge/README.md +53 -0
- package/dist/engineering-pa-core/knowledge/canonical/.gitkeep +0 -0
- package/dist/engineering-pa-core/knowledge/content-voice/README.md +64 -0
- package/dist/engineering-pa-core/knowledge/content-voice/banned-ai-tells-baseline.md +133 -0
- package/dist/engineering-pa-core/knowledge/content-voice/banned-ai-tells-shape.md +58 -0
- package/dist/engineering-pa-core/knowledge/content-voice/banned-ai-tells-taxonomy.md +33 -0
- package/dist/engineering-pa-core/knowledge/content-voice/content-pipeline-routing.md +58 -0
- package/dist/engineering-pa-core/knowledge/content-voice/new-tell-capture-procedure.md +40 -0
- package/dist/engineering-pa-core/knowledge/content-voice/voice-guide-shape.md +43 -0
- package/dist/engineering-pa-core/knowledge/delegation-dispatch/README.md +34 -0
- package/dist/engineering-pa-core/knowledge/delegation-dispatch/briefing-contract.md +77 -0
- package/dist/engineering-pa-core/knowledge/delegation-dispatch/delegation-modes.md +84 -0
- package/dist/engineering-pa-core/knowledge/delegation-dispatch/dispatch-envelope.md +238 -0
- package/dist/engineering-pa-core/knowledge/delegation-dispatch/task-groups-and-orchestrators.md +62 -0
- package/dist/engineering-pa-core/knowledge/delegation-dispatch/team-improvement-loop.md +48 -0
- package/dist/engineering-pa-core/knowledge/delegation-dispatch/tool-development-policy.md +48 -0
- package/dist/engineering-pa-core/knowledge/engineering-discipline/README.md +33 -0
- package/dist/engineering-pa-core/knowledge/engineering-discipline/blast-radius-and-safety.md +50 -0
- package/dist/engineering-pa-core/knowledge/engineering-discipline/investigate-root-causes.md +48 -0
- package/dist/engineering-pa-core/knowledge/engineering-discipline/role-ladder.md +57 -0
- package/dist/engineering-pa-core/knowledge/engineering-discipline/verify-runtime-behaviour.md +53 -0
- package/dist/engineering-pa-core/knowledge/engineering-discipline/worktree-isolation.md +50 -0
- package/dist/engineering-pa-core/knowledge/knowledge-memory/README.md +33 -0
- package/dist/engineering-pa-core/knowledge/knowledge-memory/claude-md-hygiene.md +47 -0
- package/dist/engineering-pa-core/knowledge/knowledge-memory/journal-and-self-improvement.md +53 -0
- package/dist/engineering-pa-core/knowledge/knowledge-memory/knowledge-retrieval-discipline.md +62 -0
- package/dist/engineering-pa-core/knowledge/knowledge-memory/librarian-synthesis-mandate.md +56 -0
- package/dist/engineering-pa-core/knowledge/knowledge-memory/staging-and-observations.md +58 -0
- package/dist/engineering-pa-core/knowledge/operator-interface/README.md +32 -0
- package/dist/engineering-pa-core/knowledge/operator-interface/communication-style.md +33 -0
- package/dist/engineering-pa-core/knowledge/operator-interface/escalation-and-correction.md +45 -0
- package/dist/engineering-pa-core/knowledge/operator-interface/push-back-discipline.md +42 -0
- package/dist/engineering-pa-core/knowledge/operator-interface/session-boot-and-end.md +56 -0
- package/dist/engineering-pa-core/knowledge/platform-tooling/README.md +37 -0
- package/dist/engineering-pa-core/knowledge/platform-tooling/desk-cli-reference.md +52 -0
- package/dist/engineering-pa-core/knowledge/platform-tooling/environment-awareness.md +91 -0
- package/dist/engineering-pa-core/knowledge/platform-tooling/mcp-servers-catalogue.md +52 -0
- package/dist/engineering-pa-core/knowledge/platform-tooling/pa-cli-reference.md +55 -0
- package/dist/engineering-pa-core/knowledge/platform-tooling/scheduling-and-launchd.md +56 -0
- package/dist/engineering-pa-core/knowledge/platform-tooling/tool-discovery-discipline.md +49 -0
- package/dist/engineering-pa-core/knowledge/platform-tooling/working-directory-vs-project-root.md +43 -0
- package/dist/engineering-pa-core/knowledge/platform-tooling/workspace-server.md +51 -0
- package/dist/engineering-pa-core/knowledge/raw/.gitkeep +0 -0
- package/dist/engineering-pa-core/knowledge/scaffold/domains.json +5 -0
- package/dist/engineering-pa-core/knowledge/schema/.gitkeep +0 -0
- package/dist/engineering-pa-core/knowledge/work-management/README.md +33 -0
- package/dist/engineering-pa-core/knowledge/work-management/desk-cli.md +84 -0
- package/dist/engineering-pa-core/knowledge/work-management/progress-md.md +48 -0
- package/dist/engineering-pa-core/knowledge/work-management/projects.md +64 -0
- package/dist/engineering-pa-core/knowledge/work-management/spawn-log.md +48 -0
- package/dist/engineering-pa-core/knowledge/work-management/work-items.md +67 -0
- package/dist/engineering-pa-core/package.json +26 -0
- package/dist/engineering-pa-core/roles/agent-ops/definition.md +79 -0
- package/dist/engineering-pa-core/roles/dispatch-briefer/definition.md +195 -0
- package/dist/engineering-pa-core/roles/librarian/definition.md +176 -0
- package/dist/engineering-pa-core/roles/pa/definition.md +93 -0
- package/dist/engineering-pa-core/roles/system-admin/definition.md +106 -0
- package/dist/engineering-pa-core/roles/tool-developer/definition.md +116 -0
- package/dist/engineering-pa-core/roles/writer/definition.md +130 -0
- package/dist/engineering-pa-core/schema/autonomy-envelope.schema.json +61 -0
- package/dist/engineering-pa-core/schema/project.json +114 -0
- package/dist/engineering-pa-core/schema/schedule.json +63 -0
- package/dist/engineering-pa-core/schema/work-item.json +92 -0
- package/dist/engineering-pa-core/src/config/pa-package-name.mjs +30 -0
- package/dist/engineering-pa-core/src/desk/cli.mjs +95 -0
- package/dist/engineering-pa-core/src/desk/commands.mjs +398 -0
- package/dist/engineering-pa-core/src/desk/format.mjs +84 -0
- package/dist/engineering-pa-core/src/desk/repo.mjs +172 -0
- package/dist/engineering-pa-core/src/desk/team-scope.mjs +142 -0
- package/dist/engineering-pa-core/src/distill/cli.mjs +112 -0
- package/dist/engineering-pa-core/src/distill/prereqs.mjs +171 -0
- package/dist/engineering-pa-core/src/distill/prompt.mjs +74 -0
- package/dist/engineering-pa-core/src/distill/run.mjs +230 -0
- package/dist/engineering-pa-core/src/distill/runner.mjs +86 -0
- package/dist/engineering-pa-core/src/distill/staging.mjs +59 -0
- package/dist/engineering-pa-core/src/knowledge/retrieval/backlink-indexer.mjs +154 -0
- package/dist/engineering-pa-core/src/knowledge/retrieval/graph-builder.mjs +186 -0
- package/dist/engineering-pa-core/src/knowledge/retrieval/index-builder.mjs +225 -0
- package/dist/engineering-pa-core/src/knowledge/retrieval/query.mjs +263 -0
- package/dist/engineering-pa-core/src/lock/lock.mjs +202 -0
- package/dist/engineering-pa-core/src/memory-sync/cli-support.mjs +12 -0
- package/dist/engineering-pa-core/src/memory-sync/cli.mjs +56 -0
- package/dist/engineering-pa-core/src/memory-sync/commands/pull.mjs +44 -0
- package/dist/engineering-pa-core/src/memory-sync/commands/push.mjs +46 -0
- package/dist/engineering-pa-core/src/memory-sync/mirror.mjs +114 -0
- package/dist/engineering-pa-core/src/memory-sync/repo.mjs +65 -0
- package/dist/engineering-pa-core/src/scheduler/detect.mjs +111 -0
- package/dist/engineering-pa-core/src/scheduler/host.mjs +38 -0
- package/dist/engineering-pa-core/src/scheduler/index.mjs +13 -0
- package/dist/engineering-pa-core/src/scheduler/install.mjs +854 -0
- package/dist/engineering-pa-core/src/scheduler/list.mjs +168 -0
- package/dist/engineering-pa-core/src/scheduler/remove.mjs +176 -0
- package/dist/engineering-pa-core/src/scheduler/verify.mjs +435 -0
- package/dist/engineering-pa-core/src/session-boot/autonomy-injection.mjs +84 -0
- package/dist/engineering-pa-core/src/slice/cli.mjs +139 -0
- package/dist/engineering-pa-core/src/slice/inputs.mjs +174 -0
- package/dist/engineering-pa-core/src/slice/prompt.mjs +94 -0
- package/dist/engineering-pa-core/src/slice/prompts/dave-boot.md +91 -0
- package/dist/engineering-pa-core/src/slice/run.mjs +334 -0
- package/dist/engineering-pa-core-extensions-atlassian/README.md +31 -0
- package/dist/engineering-pa-core-extensions-atlassian/index.mjs +9 -0
- package/dist/engineering-pa-core-extensions-atlassian/package.json +19 -0
- package/dist/engineering-pa-core-extensions-n8n/README.md +29 -0
- package/dist/engineering-pa-core-extensions-n8n/index.mjs +8 -0
- package/dist/engineering-pa-core-extensions-n8n/package.json +19 -0
- package/dist/engineering-pa-librarian/README.md +38 -0
- package/dist/engineering-pa-librarian/index.mjs +12 -0
- package/dist/engineering-pa-librarian/package.json +19 -0
- package/dist/engineering-pa-migrate-inspect/README.md +131 -0
- package/dist/engineering-pa-migrate-inspect/bin/migrate-inspect +2 -0
- package/dist/engineering-pa-migrate-inspect/package.json +21 -0
- package/dist/engineering-pa-migrate-inspect/src/age-key-migrate.mjs +83 -0
- package/dist/engineering-pa-migrate-inspect/src/cli.mjs +97 -0
- package/dist/engineering-pa-migrate-inspect/src/inspector.mjs +237 -0
- package/dist/engineering-pa-migrate-inspect/src/platforms/linux.mjs +87 -0
- package/dist/engineering-pa-migrate-inspect/src/platforms/macos.mjs +105 -0
- package/dist/engineering-pa-migrate-inspect/src/runner.mjs +87 -0
- package/dist/engineering-pa-runbook/README.md +103 -0
- package/dist/engineering-pa-runbook/package.json +18 -0
- package/dist/engineering-pa-runbook/reference/dex-template.runbook.json +66 -0
- package/dist/engineering-pa-runbook/reference/hq-dave.runbook.json +69 -0
- package/dist/engineering-pa-runbook/reference/smarthome01.runbook.json +92 -0
- package/dist/engineering-pa-runbook/schema/inspection.schema.json +113 -0
- package/dist/engineering-pa-runbook/schema/runbook.schema.json +67 -0
- package/dist/engineering-pa-runbook/src/validator.mjs +167 -0
- package/dist/engineering-pa-scaffold/README.md +164 -0
- package/dist/engineering-pa-scaffold/bin/pa-init +2 -0
- package/dist/engineering-pa-scaffold/index.mjs +42 -0
- package/dist/engineering-pa-scaffold/package.json +22 -0
- package/dist/engineering-pa-scaffold/src/cli.mjs +115 -0
- package/dist/engineering-pa-scaffold/src/commands/init.mjs +169 -0
- package/dist/engineering-pa-scaffold/src/generator.mjs +354 -0
- package/dist/engineering-pa-scaffold/src/prompts.mjs +125 -0
- package/dist/engineering-pa-scaffold/src/runner.mjs +142 -0
- package/dist/engineering-pa-scaffold/templates/CLAUDE.md.template +196 -0
- package/dist/engineering-pa-scaffold/templates/operator-README.md.template +44 -0
- package/dist/engineering-pa-types-engineering/README.md +35 -0
- package/dist/engineering-pa-types-engineering/docs/commands/product-build.md +120 -0
- package/dist/engineering-pa-types-engineering/index.mjs +9 -0
- package/dist/engineering-pa-types-engineering/package.json +19 -0
- package/dist/engineering-pa-types-engineering/roles/product-engineer/definition.md +650 -0
- package/dist/engineering-pa-types-engineering/roles/product-engineer/templates/build-spec.md +82 -0
- package/dist/engineering-pa-types-engineering/roles/product-engineer/templates/preflight-checklist.md +158 -0
- package/dist/engineering-pa-types-engineering/roles/product-engineer/templates/worker-brief.md +130 -0
- package/dist/engineering-pa-workspace/README.md +197 -0
- package/dist/engineering-pa-workspace/app/assets/md.js +187 -0
- package/dist/engineering-pa-workspace/app/assets/modal.js +120 -0
- package/dist/engineering-pa-workspace/app/assets/theme.css +132 -0
- package/dist/engineering-pa-workspace/app/assets/theme.js +159 -0
- package/dist/engineering-pa-workspace/app/banner.js +320 -0
- package/dist/engineering-pa-workspace/app/delegations.html +345 -0
- package/dist/engineering-pa-workspace/app/home.html +1060 -0
- package/dist/engineering-pa-workspace/app/index.html +361 -0
- package/dist/engineering-pa-workspace/app/knowledge-map.html +163 -0
- package/dist/engineering-pa-workspace/app/knowledge.html +304 -0
- package/dist/engineering-pa-workspace/app/nav.json +75 -0
- package/dist/engineering-pa-workspace/app/notes.html +199 -0
- package/dist/engineering-pa-workspace/app/onboarding.html +592 -0
- package/dist/engineering-pa-workspace/app/profile.html +243 -0
- package/dist/engineering-pa-workspace/app/project-detail.html +432 -0
- package/dist/engineering-pa-workspace/app/projects.html +297 -0
- package/dist/engineering-pa-workspace/app/repos.html +598 -0
- package/dist/engineering-pa-workspace/app/schedules.html +486 -0
- package/dist/engineering-pa-workspace/app/secrets.html +164 -0
- package/dist/engineering-pa-workspace/app/staging.html +319 -0
- package/dist/engineering-pa-workspace/app/tasks.html +465 -0
- package/dist/engineering-pa-workspace/app/vendor/chart.umd.min.js +21 -0
- package/dist/engineering-pa-workspace/app/viewer.html +116 -0
- package/dist/engineering-pa-workspace/docs/DAEMON.md +104 -0
- package/dist/engineering-pa-workspace/docs/page-blueprint.md +286 -0
- package/dist/engineering-pa-workspace/docs/status-mon.md +168 -0
- package/dist/engineering-pa-workspace/index.mjs +12 -0
- package/dist/engineering-pa-workspace/nav.json.example +8 -0
- package/dist/engineering-pa-workspace/package.json +27 -0
- package/dist/engineering-pa-workspace/src/api.mjs +2086 -0
- package/dist/engineering-pa-workspace/src/install-daemon.mjs +573 -0
- package/dist/engineering-pa-workspace/src/nav.mjs +190 -0
- package/dist/engineering-pa-workspace/src/secrets-api.mjs +175 -0
- package/dist/engineering-pa-workspace/src/server.mjs +516 -0
- package/dist/engineering-pa-workspace/src/status-mon/poll.mjs +220 -0
- package/dist/engineering-pa-workspace/src/status-mon/poller.mjs +113 -0
- package/dist/engineering-pa-workspace/src/status-mon/run.mjs +133 -0
- package/dist/engineering-pa-workspace/src/supervisor.mjs +264 -0
- package/dist/engineering-pa-workspace/src/ws.mjs +633 -0
- package/index.mjs +60 -0
- package/package.json +58 -0
- package/templates/banned-ai-tells.md +102 -0
- package/templates/purpose.md +105 -0
- package/templates/voice-guide.md +98 -0
|
@@ -0,0 +1,710 @@
|
|
|
1
|
+
// graph.mjs — knowledge-graph loader + BFS primitives for `pa knowledge`.
|
|
2
|
+
//
|
|
3
|
+
// The operator's knowledge graph lives at
|
|
4
|
+
// `<operator-root>/operator/knowledge/.graph.json`. Its shape (as of 2026-07):
|
|
5
|
+
//
|
|
6
|
+
// {
|
|
7
|
+
// version: '1.0',
|
|
8
|
+
// generatedAt: '<ISO timestamp>',
|
|
9
|
+
// nodes: [ { id: '<slug>', title: '<display>', domain: '<name>' | null } ],
|
|
10
|
+
// edges: [ { from: '<slug>', to: '<slug>', type: '<name>' } ]
|
|
11
|
+
// }
|
|
12
|
+
//
|
|
13
|
+
// Edge types currently observed: `link`, `backlink`. Design assumption:
|
|
14
|
+
// more types may appear (e.g. `supersedes`, `contradicts`); output must
|
|
15
|
+
// carry the type so a caller can filter downstream. We do NOT filter by
|
|
16
|
+
// type at this layer — that is a future flag if the need arises.
|
|
17
|
+
//
|
|
18
|
+
// Slug matching. The graph's `id` field is a path-like slug — sometimes
|
|
19
|
+
// a bare filename (`principles.md`), sometimes a repo-relative path
|
|
20
|
+
// (`operator/knowledge/clients/clive/accounts.md`). `resolveSlug` accepts
|
|
21
|
+
// either an exact match, a suffix match on the id, or a match against
|
|
22
|
+
// the title (case-insensitive). Ambiguous matches (>1 hit) throw with
|
|
23
|
+
// the candidate list — the caller then either accepts (search) or
|
|
24
|
+
// bails (related, path).
|
|
25
|
+
|
|
26
|
+
import { readFile } from 'node:fs/promises';
|
|
27
|
+
import { existsSync } from 'node:fs';
|
|
28
|
+
import { join, resolve, dirname } from 'node:path';
|
|
29
|
+
|
|
30
|
+
const GRAPH_RELATIVE_PATH = 'operator/knowledge/.graph.json';
|
|
31
|
+
|
|
32
|
+
// --- loader ---------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
export async function loadGraph({ repoRoot, graphPath } = {}) {
|
|
35
|
+
const path = graphPath ?? join(repoRoot, GRAPH_RELATIVE_PATH);
|
|
36
|
+
if (!existsSync(path)) {
|
|
37
|
+
const err = new Error(`knowledge graph not found at ${path}`);
|
|
38
|
+
err.exitCode = 3;
|
|
39
|
+
throw err;
|
|
40
|
+
}
|
|
41
|
+
let raw;
|
|
42
|
+
try {
|
|
43
|
+
raw = await readFile(path, 'utf8');
|
|
44
|
+
} catch (e) {
|
|
45
|
+
const err = new Error(`could not read knowledge graph at ${path}: ${e.message}`);
|
|
46
|
+
err.exitCode = 3;
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
let json;
|
|
50
|
+
try {
|
|
51
|
+
json = JSON.parse(raw);
|
|
52
|
+
} catch (e) {
|
|
53
|
+
const err = new Error(`could not parse knowledge graph at ${path}: ${e.message}`);
|
|
54
|
+
err.exitCode = 3;
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
57
|
+
if (!Array.isArray(json.nodes) || !Array.isArray(json.edges)) {
|
|
58
|
+
const err = new Error(`knowledge graph at ${path} is missing nodes[] or edges[]`);
|
|
59
|
+
err.exitCode = 3;
|
|
60
|
+
throw err;
|
|
61
|
+
}
|
|
62
|
+
return json;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// --- adjacency ------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
// Build outbound + inbound adjacency maps keyed by node id. Each entry
|
|
68
|
+
// carries the destination id and the edge type — the caller decides
|
|
69
|
+
// whether to filter or annotate.
|
|
70
|
+
export function buildAdjacency(graph) {
|
|
71
|
+
const out = new Map();
|
|
72
|
+
const in_ = new Map();
|
|
73
|
+
for (const node of graph.nodes) {
|
|
74
|
+
out.set(node.id, []);
|
|
75
|
+
in_.set(node.id, []);
|
|
76
|
+
}
|
|
77
|
+
for (const edge of graph.edges) {
|
|
78
|
+
// Silently skip edges to/from unknown nodes rather than reject the
|
|
79
|
+
// whole graph — the generator may have observed a dangling link.
|
|
80
|
+
if (!out.has(edge.from) || !in_.has(edge.to)) continue;
|
|
81
|
+
out.get(edge.from).push({ id: edge.to, type: edge.type });
|
|
82
|
+
in_.get(edge.to).push({ id: edge.from, type: edge.type });
|
|
83
|
+
}
|
|
84
|
+
return { out, in: in_ };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// --- slug resolution ------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
// Resolve a user-supplied slug against the graph. Match order:
|
|
90
|
+
// 1. Exact id match.
|
|
91
|
+
// 2. Path-suffix match on id (`accounts.md` matches
|
|
92
|
+
// `operator/knowledge/clients/clive/accounts.md`).
|
|
93
|
+
// 3. Title match (case-insensitive, substring).
|
|
94
|
+
//
|
|
95
|
+
// Returns { matches: [nodeId, ...] }. The caller decides what to do
|
|
96
|
+
// with 0 / 1 / N results.
|
|
97
|
+
export function resolveSlug(graph, slug) {
|
|
98
|
+
const q = slug.toLowerCase();
|
|
99
|
+
const byId = new Map();
|
|
100
|
+
const bySuffix = [];
|
|
101
|
+
const byTitle = [];
|
|
102
|
+
for (const node of graph.nodes) {
|
|
103
|
+
if (node.id === slug) return { matches: [node.id] };
|
|
104
|
+
byId.set(node.id.toLowerCase(), node);
|
|
105
|
+
if (node.id.toLowerCase().endsWith('/' + q) || node.id.toLowerCase() === q) {
|
|
106
|
+
bySuffix.push(node.id);
|
|
107
|
+
}
|
|
108
|
+
if (node.title && node.title.toLowerCase().includes(q)) {
|
|
109
|
+
byTitle.push(node.id);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Case-insensitive exact match on id?
|
|
113
|
+
if (byId.has(q)) return { matches: [byId.get(q).id] };
|
|
114
|
+
if (bySuffix.length > 0) return { matches: dedupe(bySuffix) };
|
|
115
|
+
if (byTitle.length > 0) return { matches: dedupe(byTitle) };
|
|
116
|
+
return { matches: [] };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function dedupe(xs) {
|
|
120
|
+
return Array.from(new Set(xs));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// --- BFS ------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
// Shortest path between two node ids. Returns an array of `{ id, type }`
|
|
126
|
+
// entries where `type` is the edge type used to reach that node (null for
|
|
127
|
+
// the starting node). Direction: undirected — the graph is treated as
|
|
128
|
+
// symmetric for path-finding, so a `path a b` and `path b a` produce
|
|
129
|
+
// mirror-image results. Returns null if no path.
|
|
130
|
+
export function shortestPath(graph, from, to) {
|
|
131
|
+
if (from === to) return [{ id: from, type: null }];
|
|
132
|
+
const adjacency = buildUndirected(graph);
|
|
133
|
+
const visited = new Set([from]);
|
|
134
|
+
const parents = new Map();
|
|
135
|
+
const queue = [from];
|
|
136
|
+
while (queue.length > 0) {
|
|
137
|
+
const current = queue.shift();
|
|
138
|
+
const neighbours = adjacency.get(current) || [];
|
|
139
|
+
for (const { id: next, type } of neighbours) {
|
|
140
|
+
if (visited.has(next)) continue;
|
|
141
|
+
visited.add(next);
|
|
142
|
+
parents.set(next, { from: current, type });
|
|
143
|
+
if (next === to) return reconstructPath(from, to, parents);
|
|
144
|
+
queue.push(next);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function buildUndirected(graph) {
|
|
151
|
+
const adj = new Map();
|
|
152
|
+
for (const node of graph.nodes) adj.set(node.id, []);
|
|
153
|
+
for (const edge of graph.edges) {
|
|
154
|
+
if (!adj.has(edge.from) || !adj.has(edge.to)) continue;
|
|
155
|
+
adj.get(edge.from).push({ id: edge.to, type: edge.type });
|
|
156
|
+
adj.get(edge.to).push({ id: edge.from, type: edge.type });
|
|
157
|
+
}
|
|
158
|
+
return adj;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function reconstructPath(from, to, parents) {
|
|
162
|
+
const out = [{ id: to, type: parents.get(to).type }];
|
|
163
|
+
let cursor = to;
|
|
164
|
+
while (cursor !== from) {
|
|
165
|
+
const p = parents.get(cursor);
|
|
166
|
+
if (!p) return null; // shouldn't happen
|
|
167
|
+
if (p.from === from) {
|
|
168
|
+
out.unshift({ id: from, type: null });
|
|
169
|
+
return out;
|
|
170
|
+
}
|
|
171
|
+
out.unshift({ id: p.from, type: parents.get(p.from).type });
|
|
172
|
+
cursor = p.from;
|
|
173
|
+
}
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// --- verb: related --------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
// 1-hop neighbourhood. `direction` is 'both' (default) | 'out' | 'in'.
|
|
180
|
+
// Returns { anchor: nodeId, neighbours: [{id, title, domain, type, direction}] }.
|
|
181
|
+
export function related(graph, slug, { direction = 'both' } = {}) {
|
|
182
|
+
const resolved = resolveSlug(graph, slug);
|
|
183
|
+
if (resolved.matches.length === 0) {
|
|
184
|
+
const err = new Error(`unknown slug: ${slug}`);
|
|
185
|
+
err.exitCode = 2;
|
|
186
|
+
throw err;
|
|
187
|
+
}
|
|
188
|
+
if (resolved.matches.length > 1) {
|
|
189
|
+
const err = new Error(
|
|
190
|
+
`ambiguous slug: ${slug} — matches ${resolved.matches.length} nodes:\n ${resolved.matches.slice(0, 10).join('\n ')}` +
|
|
191
|
+
(resolved.matches.length > 10 ? `\n ...` : ''),
|
|
192
|
+
);
|
|
193
|
+
err.exitCode = 2;
|
|
194
|
+
throw err;
|
|
195
|
+
}
|
|
196
|
+
const anchor = resolved.matches[0];
|
|
197
|
+
const adj = buildAdjacency(graph);
|
|
198
|
+
const byId = nodeIndex(graph);
|
|
199
|
+
const results = [];
|
|
200
|
+
if (direction === 'both' || direction === 'out') {
|
|
201
|
+
for (const { id, type } of adj.out.get(anchor) || []) {
|
|
202
|
+
results.push(annotate(byId, id, type, 'out'));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (direction === 'both' || direction === 'in') {
|
|
206
|
+
for (const { id, type } of adj.in.get(anchor) || []) {
|
|
207
|
+
results.push(annotate(byId, id, type, 'in'));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return { anchor, neighbours: results };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// --- verb: search ---------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
// Title-only search by default. `full` flag toggles doc-content scan (which
|
|
216
|
+
// requires `repoRoot` so we can read the file).
|
|
217
|
+
//
|
|
218
|
+
// Matching: the query is tokenised on whitespace. A node matches when
|
|
219
|
+
// EVERY token matches (substring, case-insensitive) against its id or
|
|
220
|
+
// title — so `search "barry voice"` finds a doc titled "Barry voice guide"
|
|
221
|
+
// AND a doc whose fields carry the two tokens separately (e.g. id
|
|
222
|
+
// `.../writing/barry-voice.md` split by the hyphen). The historical
|
|
223
|
+
// whole-query substring behaviour is preserved: single-token queries are
|
|
224
|
+
// unchanged, and an exact-phrase substring hit scores the same as it
|
|
225
|
+
// always did.
|
|
226
|
+
//
|
|
227
|
+
// Ranking: exact title/id match (score 3) > whole-query substring or
|
|
228
|
+
// all-tokens hit on title/id (score 2) > body hit (score 1, `--full`
|
|
229
|
+
// only — whole-query substring in the body, or every token found across
|
|
230
|
+
// id/title/body). Tie-break within a score band: (1) higher title-token
|
|
231
|
+
// count — the number of query tokens matched in id/title (added post-#60
|
|
232
|
+
// to fix the "title-heavy doc buried under body-heavy doc" miss); then
|
|
233
|
+
// (2) higher edge count; then (3) id.
|
|
234
|
+
//
|
|
235
|
+
// Naming: this function is the STRICT AND-match. `searchCascade()` below
|
|
236
|
+
// wraps it in the fallback ladder used by the default `pa knowledge search`
|
|
237
|
+
// (Deliverable A, w-2026-07-06-020). `--strict` on the CLI dispatches
|
|
238
|
+
// straight here for scripted callers that depend on the current behaviour.
|
|
239
|
+
export async function search(graph, term, { full = false, repoRoot = null } = {}) {
|
|
240
|
+
const q = term.toLowerCase();
|
|
241
|
+
const tokens = q.split(/\s+/).filter(Boolean);
|
|
242
|
+
const multiToken = tokens.length > 1;
|
|
243
|
+
// Content tokens (non-stopword) — used ONLY for the titleTokens tie-break.
|
|
244
|
+
// The AND-match itself still requires every original token to hit, so
|
|
245
|
+
// this can't change which docs qualify; it only re-orders within a
|
|
246
|
+
// score band. See file-level cascade comment for why.
|
|
247
|
+
const contentTokens = tokens.filter((t) => !STOPWORDS.has(t));
|
|
248
|
+
const adj = buildAdjacency(graph);
|
|
249
|
+
const hits = [];
|
|
250
|
+
for (const node of graph.nodes) {
|
|
251
|
+
const title = (node.title || '').toLowerCase();
|
|
252
|
+
const id = node.id.toLowerCase();
|
|
253
|
+
const tokenInFields = (t) => title.includes(t) || id.includes(t);
|
|
254
|
+
// Count content-token overlap with id/title — load-bearing for the
|
|
255
|
+
// body-tier tie-break so a doc whose TITLE is what the caller asked
|
|
256
|
+
// about outranks one that only happens to mention every token
|
|
257
|
+
// somewhere in a rambling body. Structural stopwords ("to" is a
|
|
258
|
+
// substring of "operator") are excluded so they can't set the ranking.
|
|
259
|
+
const titleTokens = contentTokens.reduce((n, t) => n + (tokenInFields(t) ? 1 : 0), 0);
|
|
260
|
+
let score = 0;
|
|
261
|
+
if (title === q || id === q) score = 3;
|
|
262
|
+
else if (title.includes(q) || id.includes(q)) score = 2;
|
|
263
|
+
else if (multiToken && tokens.every(tokenInFields)) score = 2;
|
|
264
|
+
if (score > 0) {
|
|
265
|
+
hits.push({
|
|
266
|
+
node,
|
|
267
|
+
score,
|
|
268
|
+
titleTokens,
|
|
269
|
+
edgeCount: (adj.out.get(node.id)?.length ?? 0) + (adj.in.get(node.id)?.length ?? 0),
|
|
270
|
+
});
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (full && repoRoot) {
|
|
274
|
+
const body = await readBody(repoRoot, node.id);
|
|
275
|
+
if (body != null) {
|
|
276
|
+
const hay = body.toLowerCase();
|
|
277
|
+
const bodyHit = hay.includes(q) ||
|
|
278
|
+
(multiToken && tokens.every((t) => tokenInFields(t) || hay.includes(t)));
|
|
279
|
+
if (bodyHit) {
|
|
280
|
+
hits.push({
|
|
281
|
+
node,
|
|
282
|
+
score: 1,
|
|
283
|
+
titleTokens,
|
|
284
|
+
edgeCount: (adj.out.get(node.id)?.length ?? 0) + (adj.in.get(node.id)?.length ?? 0),
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
hits.sort((a, b) =>
|
|
291
|
+
(b.score - a.score) ||
|
|
292
|
+
(b.titleTokens - a.titleTokens) ||
|
|
293
|
+
(b.edgeCount - a.edgeCount) ||
|
|
294
|
+
a.node.id.localeCompare(b.node.id),
|
|
295
|
+
);
|
|
296
|
+
return hits.map((h) => ({
|
|
297
|
+
id: h.node.id,
|
|
298
|
+
title: h.node.title,
|
|
299
|
+
domain: h.node.domain,
|
|
300
|
+
score: h.score,
|
|
301
|
+
titleTokens: h.titleTokens,
|
|
302
|
+
edgeCount: h.edgeCount,
|
|
303
|
+
}));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// OR-match variant. Multi-token queries hit any node where AT LEAST ONE
|
|
307
|
+
// token matches (id/title, plus body when `full`). Ranked primarily by how
|
|
308
|
+
// many CONTENT tokens (non-stopword) land in id/title — a title-heavy
|
|
309
|
+
// 2-of-4 content-token doc outranks a body-heavy 4-of-4 doc, because the
|
|
310
|
+
// title says "this doc is about X" while the body only says "this doc
|
|
311
|
+
// mentions X". The stopword exclusion for the title-count is load-bearing:
|
|
312
|
+
// otherwise every doc's id contains "to" (via "operator") and the
|
|
313
|
+
// title-boost degenerates into "all docs matched 1 title token" — which
|
|
314
|
+
// puts research docs with query-echoing filenames unfairly at the top of
|
|
315
|
+
// the noisy "what to X versus Y" cases. Secondary sort: total tokens
|
|
316
|
+
// matched (all tokens, including body); then edge count; then id.
|
|
317
|
+
//
|
|
318
|
+
// Zero-token overlap is dropped. Returns rows carrying `tokensMatched`
|
|
319
|
+
// (title/id/body total, back-compat name), `tokensTotal`, plus
|
|
320
|
+
// `titleTokensMatched` (content-only) for the eval and cascade to inspect.
|
|
321
|
+
//
|
|
322
|
+
// Single-token queries collapse to a substring hit over id/title/body,
|
|
323
|
+
// giving the caller a graceful fallback when `search()` (AND) turned up
|
|
324
|
+
// empty but a related term appears in a body somewhere.
|
|
325
|
+
export async function searchOr(graph, term, { full = false, repoRoot = null } = {}) {
|
|
326
|
+
const q = term.toLowerCase();
|
|
327
|
+
const tokens = q.split(/\s+/).filter(Boolean);
|
|
328
|
+
const tokensTotal = tokens.length;
|
|
329
|
+
if (tokensTotal === 0) return [];
|
|
330
|
+
// Content tokens (non-stopword) drive the title-boost — see fn header.
|
|
331
|
+
// A caller can still hit on stopword tokens (via `matched`), but a
|
|
332
|
+
// stopword substring in a doc id shouldn't confer title-relevance.
|
|
333
|
+
const contentTokens = tokens.filter((t) => !STOPWORDS.has(t));
|
|
334
|
+
const adj = buildAdjacency(graph);
|
|
335
|
+
const hits = [];
|
|
336
|
+
for (const node of graph.nodes) {
|
|
337
|
+
const title = (node.title || '').toLowerCase();
|
|
338
|
+
const id = node.id.toLowerCase();
|
|
339
|
+
let body = null;
|
|
340
|
+
if (full && repoRoot) body = (await readBody(repoRoot, node.id))?.toLowerCase() ?? null;
|
|
341
|
+
let titleMatched = 0;
|
|
342
|
+
for (const t of contentTokens) {
|
|
343
|
+
if (title.includes(t) || id.includes(t)) titleMatched += 1;
|
|
344
|
+
}
|
|
345
|
+
let matched = 0;
|
|
346
|
+
for (const t of tokens) {
|
|
347
|
+
const inTitleOrId = title.includes(t) || id.includes(t);
|
|
348
|
+
if (inTitleOrId || (body != null && body.includes(t))) matched += 1;
|
|
349
|
+
}
|
|
350
|
+
if (matched > 0) {
|
|
351
|
+
hits.push({
|
|
352
|
+
node,
|
|
353
|
+
titleMatched,
|
|
354
|
+
matched,
|
|
355
|
+
edgeCount: (adj.out.get(node.id)?.length ?? 0) + (adj.in.get(node.id)?.length ?? 0),
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
hits.sort((a, b) =>
|
|
360
|
+
(b.titleMatched - a.titleMatched) ||
|
|
361
|
+
(b.matched - a.matched) ||
|
|
362
|
+
(b.edgeCount - a.edgeCount) ||
|
|
363
|
+
a.node.id.localeCompare(b.node.id),
|
|
364
|
+
);
|
|
365
|
+
return hits.map((h) => ({
|
|
366
|
+
id: h.node.id,
|
|
367
|
+
title: h.node.title,
|
|
368
|
+
domain: h.node.domain,
|
|
369
|
+
score: 0,
|
|
370
|
+
tokensMatched: h.matched,
|
|
371
|
+
titleTokensMatched: h.titleMatched,
|
|
372
|
+
tokensTotal,
|
|
373
|
+
edgeCount: h.edgeCount,
|
|
374
|
+
}));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Zero-result fallback cascade for `pa knowledge search` (Deliverable A,
|
|
378
|
+
// w-2026-07-06-020; stopword + title-boost pass w-2026-07-08-008).
|
|
379
|
+
//
|
|
380
|
+
// The original audit failure mode was silent misses: a caller types a
|
|
381
|
+
// natural phrasing, gets `(no matches)`, and reads that as an authoritative
|
|
382
|
+
// "not documented." First-live-eval (2026-07-08) surfaced the sibling
|
|
383
|
+
// failure mode: stopword dilution. Queries like "spec and review workflow"
|
|
384
|
+
// or "when does concurrent work need a project" AND-match nothing on title
|
|
385
|
+
// (grammatical filler drags the requirement), then the body-tier ranker
|
|
386
|
+
// (all body-hits score 1, tie-break edge count) pulls body-heavy random
|
|
387
|
+
// docs above the title-heavy target. Tiers:
|
|
388
|
+
//
|
|
389
|
+
// 1. mode:'title' — AND match on id/title with the caller's tokens.
|
|
390
|
+
// 1b (still 'title') — same, with English stopwords stripped. Fires only
|
|
391
|
+
// if stripping actually removed a token. Monotonic:
|
|
392
|
+
// fewer required tokens → strict superset of hits.
|
|
393
|
+
// 2. mode:'body' — AND match retried against the body (`--full`).
|
|
394
|
+
// Considered against tier 3 (below) before returning.
|
|
395
|
+
// 3. mode:'partial' — OR match ranked title-tokens-first (see searchOr).
|
|
396
|
+
// PROMOTED past tier 2 when its top result has more
|
|
397
|
+
// title-token overlap than tier 2's — the "title
|
|
398
|
+
// says what the doc is about" heuristic.
|
|
399
|
+
// 4. mode:'miss' — genuine miss. Envelope carries a `hint` with
|
|
400
|
+
// nearest domain names so the caller's next move is
|
|
401
|
+
// a browse rather than a guess.
|
|
402
|
+
//
|
|
403
|
+
// Envelope shape (JSON-stable; consumed by the eval runner and formatters):
|
|
404
|
+
// { mode, results, tokensTotal?, hint? { nearestDomains: string[] } }
|
|
405
|
+
//
|
|
406
|
+
// Single-term hits at tier 1 stay byte-identical to the pre-cascade world —
|
|
407
|
+
// this is a load-bearing invariant for scripted callers that grep the
|
|
408
|
+
// table output; see the corresponding CLI test.
|
|
409
|
+
export async function searchCascade(graph, term, { full = false, repoRoot = null } = {}) {
|
|
410
|
+
// Tier 1: title-AND on original tokens (unchanged behaviour).
|
|
411
|
+
const t1 = await search(graph, term, { full, repoRoot });
|
|
412
|
+
if (t1.length > 0) return { mode: 'title', results: t1 };
|
|
413
|
+
|
|
414
|
+
const originalTokens = term.toLowerCase().split(/\s+/).filter(Boolean);
|
|
415
|
+
const strippedTokens = filterStopwords(originalTokens);
|
|
416
|
+
const strippedDiffers = strippedTokens.length > 0 && strippedTokens.length < originalTokens.length;
|
|
417
|
+
const strippedTerm = strippedTokens.join(' ');
|
|
418
|
+
|
|
419
|
+
// Tier 1b: title-AND on stopword-stripped tokens. Only fires when
|
|
420
|
+
// stripping actually removed content — otherwise it's identical to tier 1
|
|
421
|
+
// and would double the work for nothing.
|
|
422
|
+
if (strippedDiffers) {
|
|
423
|
+
const t1b = await search(graph, strippedTerm, { full, repoRoot });
|
|
424
|
+
if (t1b.length > 0) return { mode: 'title', results: t1b };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Tier 2: body-AND. Try original tokens first, then stripped as a widening
|
|
428
|
+
// fallback. We union rather than replace — the stripped run can only ADD
|
|
429
|
+
// hits, and the sort inside search() puts the best (title-heaviest)
|
|
430
|
+
// first, so unioning is safe.
|
|
431
|
+
let t2 = [];
|
|
432
|
+
if (!full) {
|
|
433
|
+
t2 = await search(graph, term, { full: true, repoRoot });
|
|
434
|
+
if (strippedDiffers) {
|
|
435
|
+
const t2b = await search(graph, strippedTerm, { full: true, repoRoot });
|
|
436
|
+
t2 = unionByIdKeepBest(t2, t2b);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Tier 3: OR on ORIGINAL tokens. Load-bearing: using original (not
|
|
441
|
+
// stripped) preserves the discriminating power of the caller's full
|
|
442
|
+
// query — a 5-token query has 5 possible tokens to match; if we ran on
|
|
443
|
+
// the stripped 3-token set, docs matching the 3 stripped tokens would
|
|
444
|
+
// compress into a single tier and the target (which may only match 2)
|
|
445
|
+
// gets buried. The title-boost inside searchOr does the real ranking
|
|
446
|
+
// work here; stripping the OR set turned out to REGRESS the noisier
|
|
447
|
+
// "what to escalate versus execute" case in first-live eval, so we
|
|
448
|
+
// keep the original tokens. Multi-token only — a single-token OR
|
|
449
|
+
// degenerates to a substring hit over id/title/body, which the
|
|
450
|
+
// callers of the previous cascade version did not expect.
|
|
451
|
+
let t3 = [];
|
|
452
|
+
if (originalTokens.length > 1) {
|
|
453
|
+
t3 = await searchOr(graph, term, { full: true, repoRoot });
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Decide between tier 2 and tier 3. If tier 3's top result has strictly
|
|
457
|
+
// more title-token overlap than tier 2's top result, use tier 3 — that's
|
|
458
|
+
// the case where an AND-match trapped the ranker on a body-heavy doc
|
|
459
|
+
// (e.g. `writing/README.md` for "spec and review workflow") when a
|
|
460
|
+
// title-heavy doc (`spec-and-review.md`) is what the caller wants.
|
|
461
|
+
const t2TopTitleTokens = t2.length > 0 ? (t2[0].titleTokens ?? 0) : -1;
|
|
462
|
+
const t3TopTitleTokens = t3.length > 0 ? (t3[0].titleTokensMatched ?? 0) : -1;
|
|
463
|
+
if (t2.length > 0 && t3TopTitleTokens > t2TopTitleTokens) {
|
|
464
|
+
return { mode: 'partial', results: t3, tokensTotal: originalTokens.length };
|
|
465
|
+
}
|
|
466
|
+
if (t2.length > 0) {
|
|
467
|
+
return { mode: 'body', results: t2 };
|
|
468
|
+
}
|
|
469
|
+
if (t3.length > 0) {
|
|
470
|
+
return { mode: 'partial', results: t3, tokensTotal: originalTokens.length };
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return { mode: 'miss', results: [], hint: guidedMissHint(graph, term) };
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Union two search() result arrays by id, keeping the row with the highest
|
|
477
|
+
// (score, titleTokens, edgeCount). Preserves search()'s stable sort order
|
|
478
|
+
// afterwards. Used to fold tier-2 (original tokens) and tier-2b (stripped
|
|
479
|
+
// tokens) into a single body-tier ranking without letting the stripped
|
|
480
|
+
// pass shift the winners the original pass already ordered.
|
|
481
|
+
function unionByIdKeepBest(a, b) {
|
|
482
|
+
const bestById = new Map();
|
|
483
|
+
const better = (x, y) => {
|
|
484
|
+
if (x.score !== y.score) return x.score > y.score;
|
|
485
|
+
if ((x.titleTokens ?? 0) !== (y.titleTokens ?? 0)) return (x.titleTokens ?? 0) > (y.titleTokens ?? 0);
|
|
486
|
+
if (x.edgeCount !== y.edgeCount) return x.edgeCount > y.edgeCount;
|
|
487
|
+
return false;
|
|
488
|
+
};
|
|
489
|
+
for (const row of [...a, ...b]) {
|
|
490
|
+
const prev = bestById.get(row.id);
|
|
491
|
+
if (!prev || better(row, prev)) bestById.set(row.id, row);
|
|
492
|
+
}
|
|
493
|
+
const merged = [...bestById.values()];
|
|
494
|
+
merged.sort((x, y) =>
|
|
495
|
+
(y.score - x.score) ||
|
|
496
|
+
((y.titleTokens ?? 0) - (x.titleTokens ?? 0)) ||
|
|
497
|
+
(y.edgeCount - x.edgeCount) ||
|
|
498
|
+
x.id.localeCompare(y.id),
|
|
499
|
+
);
|
|
500
|
+
return merged;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Common English stopwords — grammatical function words only. Anything
|
|
504
|
+
// that could reasonably appear in a knowledge-doc TITLE is deliberately
|
|
505
|
+
// NOT here (workflow, routine, process, guide, guidelines, patterns,
|
|
506
|
+
// discipline, standard, all, every, use — all title-plausible content).
|
|
507
|
+
// The list is articles, conjunctions, prepositions, auxiliary/copula
|
|
508
|
+
// verbs, question pronouns, demonstratives, personal pronouns, and a
|
|
509
|
+
// small tail of very high-frequency discourse fillers ("need", "want")
|
|
510
|
+
// that appear in "how do I ..." phrasings and never in doc titles here.
|
|
511
|
+
//
|
|
512
|
+
// If a query is nothing but stopwords ("the and or"), `filterStopwords`
|
|
513
|
+
// returns the original set rather than an empty one — a nonsense query
|
|
514
|
+
// still goes through, just not any differently from before.
|
|
515
|
+
export const STOPWORDS = new Set([
|
|
516
|
+
// articles
|
|
517
|
+
'a', 'an', 'the',
|
|
518
|
+
// conjunctions
|
|
519
|
+
'and', 'or', 'but', 'nor', 'so', 'yet',
|
|
520
|
+
// prepositions
|
|
521
|
+
'of', 'in', 'on', 'at', 'to', 'for', 'with', 'by', 'from',
|
|
522
|
+
'into', 'onto', 'over', 'under', 'about', 'against', 'between',
|
|
523
|
+
// auxiliary + copula verbs
|
|
524
|
+
'is', 'are', 'was', 'were', 'be', 'been', 'being', 'am',
|
|
525
|
+
'do', 'does', 'did', 'doing',
|
|
526
|
+
'have', 'has', 'had', 'having',
|
|
527
|
+
'will', 'would', 'shall', 'should', 'can', 'could', 'may', 'might', 'must',
|
|
528
|
+
// question / interrogative pronouns
|
|
529
|
+
'when', 'where', 'what', 'which', 'who', 'whom', 'why', 'how',
|
|
530
|
+
// demonstratives
|
|
531
|
+
'this', 'that', 'these', 'those',
|
|
532
|
+
// personal pronouns
|
|
533
|
+
'i', 'you', 'he', 'she', 'it', 'we', 'they', 'them',
|
|
534
|
+
'my', 'your', 'his', 'her', 'our', 'their', 'its',
|
|
535
|
+
// discourse verbs / fillers common in "how do I …" phrasings.
|
|
536
|
+
// Deliberate scope: no title in the current graph uses these; if a
|
|
537
|
+
// future doc titled "user needs" or "want-triggered flows" lands,
|
|
538
|
+
// trim this section rather than reasoning around it.
|
|
539
|
+
'need', 'needs', 'needed', 'want', 'wants',
|
|
540
|
+
// small connectives
|
|
541
|
+
'as', 'if', 'than', 'then',
|
|
542
|
+
]);
|
|
543
|
+
|
|
544
|
+
export function filterStopwords(tokens) {
|
|
545
|
+
const filtered = tokens.filter((t) => !STOPWORDS.has(t.toLowerCase()));
|
|
546
|
+
return filtered.length > 0 ? filtered : tokens.slice();
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Rank domains by relevance to the miss so the caller can pivot to
|
|
550
|
+
// `pa knowledge domain <name>` rather than re-derive from scratch. Score:
|
|
551
|
+
// +2 per token that appears as a substring of the domain name
|
|
552
|
+
// +1 per doc in the domain whose id or title carries any token
|
|
553
|
+
// Falls back to the top-N-by-node-count domains when nothing scores — the
|
|
554
|
+
// operator still gets a browsable index of the graph rather than a bare
|
|
555
|
+
// `(no matches)`.
|
|
556
|
+
function guidedMissHint(graph, term) {
|
|
557
|
+
const tokens = term.toLowerCase().split(/\s+/).filter(Boolean);
|
|
558
|
+
const domains = uniqueDomains(graph);
|
|
559
|
+
const scores = new Map();
|
|
560
|
+
for (const d of domains) scores.set(d, 0);
|
|
561
|
+
for (const d of domains) {
|
|
562
|
+
const dl = d.toLowerCase();
|
|
563
|
+
for (const t of tokens) if (dl.includes(t)) scores.set(d, scores.get(d) + 2);
|
|
564
|
+
}
|
|
565
|
+
for (const node of graph.nodes) {
|
|
566
|
+
if (!node.domain) continue;
|
|
567
|
+
const title = (node.title || '').toLowerCase();
|
|
568
|
+
const id = node.id.toLowerCase();
|
|
569
|
+
let touched = false;
|
|
570
|
+
for (const t of tokens) if (title.includes(t) || id.includes(t)) { touched = true; break; }
|
|
571
|
+
if (touched) scores.set(node.domain, (scores.get(node.domain) ?? 0) + 1);
|
|
572
|
+
}
|
|
573
|
+
const counts = new Map();
|
|
574
|
+
for (const node of graph.nodes) if (node.domain) counts.set(node.domain, (counts.get(node.domain) ?? 0) + 1);
|
|
575
|
+
const ranked = [...scores.entries()].sort((a, b) => {
|
|
576
|
+
if (b[1] !== a[1]) return b[1] - a[1];
|
|
577
|
+
return (counts.get(b[0]) ?? 0) - (counts.get(a[0]) ?? 0);
|
|
578
|
+
});
|
|
579
|
+
const top = ranked.filter(([, s]) => s > 0).slice(0, 3).map(([d]) => d);
|
|
580
|
+
if (top.length > 0) return { nearestDomains: top };
|
|
581
|
+
return { nearestDomains: ranked.slice(0, 3).map(([d]) => d) };
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
async function readBody(repoRoot, id) {
|
|
585
|
+
// The id is either a path relative to the repo root, or a bare slug.
|
|
586
|
+
// We try repoRoot/<id> first, then repoRoot/operator/knowledge/<id>.
|
|
587
|
+
const candidates = [
|
|
588
|
+
resolve(repoRoot, id),
|
|
589
|
+
resolve(repoRoot, 'operator', 'knowledge', id),
|
|
590
|
+
];
|
|
591
|
+
for (const path of candidates) {
|
|
592
|
+
if (existsSync(path)) {
|
|
593
|
+
try {
|
|
594
|
+
return await readFile(path, 'utf8');
|
|
595
|
+
} catch { return null; }
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// --- verb: domain ---------------------------------------------------------
|
|
602
|
+
|
|
603
|
+
// Return every node in the named domain + the edges between them.
|
|
604
|
+
export function domain(graph, name) {
|
|
605
|
+
const nodes = graph.nodes.filter((n) => n.domain === name);
|
|
606
|
+
if (nodes.length === 0) {
|
|
607
|
+
const known = uniqueDomains(graph);
|
|
608
|
+
const err = new Error(
|
|
609
|
+
`unknown domain: ${name}\navailable domains: ${known.join(', ') || '(none)'}`,
|
|
610
|
+
);
|
|
611
|
+
err.exitCode = 2;
|
|
612
|
+
throw err;
|
|
613
|
+
}
|
|
614
|
+
const nodeIds = new Set(nodes.map((n) => n.id));
|
|
615
|
+
const edges = graph.edges.filter((e) => nodeIds.has(e.from) && nodeIds.has(e.to));
|
|
616
|
+
return { domain: name, nodes, edges };
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// --- verb: orphans --------------------------------------------------------
|
|
620
|
+
|
|
621
|
+
// Nodes with zero inbound edges. Excludes null-domain nodes (auto-generated
|
|
622
|
+
// stubs) if `includeStubs` is false. Default is to include everything so
|
|
623
|
+
// operators see the full picture.
|
|
624
|
+
export function orphans(graph, { includeStubs = true } = {}) {
|
|
625
|
+
const adj = buildAdjacency(graph);
|
|
626
|
+
const results = [];
|
|
627
|
+
for (const node of graph.nodes) {
|
|
628
|
+
if (!includeStubs && node.domain == null) continue;
|
|
629
|
+
if ((adj.in.get(node.id)?.length ?? 0) === 0) {
|
|
630
|
+
results.push({ id: node.id, title: node.title, domain: node.domain, outDegree: adj.out.get(node.id)?.length ?? 0 });
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
results.sort((a, b) => (a.domain ?? '').localeCompare(b.domain ?? '') || a.id.localeCompare(b.id));
|
|
634
|
+
return results;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// --- verb: path -----------------------------------------------------------
|
|
638
|
+
|
|
639
|
+
// Shortest path between two slugs. Wraps `shortestPath` with slug
|
|
640
|
+
// resolution + node-index annotation.
|
|
641
|
+
export function path(graph, a, b) {
|
|
642
|
+
const from = resolveSingle(graph, a, 'from');
|
|
643
|
+
const to = resolveSingle(graph, b, 'to');
|
|
644
|
+
const raw = shortestPath(graph, from, to);
|
|
645
|
+
if (raw == null) return { from, to, path: null };
|
|
646
|
+
const byId = nodeIndex(graph);
|
|
647
|
+
return {
|
|
648
|
+
from,
|
|
649
|
+
to,
|
|
650
|
+
path: raw.map((step) => ({
|
|
651
|
+
id: step.id,
|
|
652
|
+
title: byId.get(step.id)?.title ?? null,
|
|
653
|
+
domain: byId.get(step.id)?.domain ?? null,
|
|
654
|
+
type: step.type,
|
|
655
|
+
})),
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function resolveSingle(graph, slug, label) {
|
|
660
|
+
const r = resolveSlug(graph, slug);
|
|
661
|
+
if (r.matches.length === 0) {
|
|
662
|
+
const err = new Error(`unknown slug (${label}): ${slug}`);
|
|
663
|
+
err.exitCode = 2;
|
|
664
|
+
throw err;
|
|
665
|
+
}
|
|
666
|
+
if (r.matches.length > 1) {
|
|
667
|
+
const err = new Error(
|
|
668
|
+
`ambiguous slug (${label}): ${slug} — matches ${r.matches.length} nodes:\n ${r.matches.slice(0, 10).join('\n ')}` +
|
|
669
|
+
(r.matches.length > 10 ? `\n ...` : ''),
|
|
670
|
+
);
|
|
671
|
+
err.exitCode = 2;
|
|
672
|
+
throw err;
|
|
673
|
+
}
|
|
674
|
+
return r.matches[0];
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// --- helpers --------------------------------------------------------------
|
|
678
|
+
|
|
679
|
+
export function nodeIndex(graph) {
|
|
680
|
+
const m = new Map();
|
|
681
|
+
for (const n of graph.nodes) m.set(n.id, n);
|
|
682
|
+
return m;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function annotate(byId, id, type, direction) {
|
|
686
|
+
const node = byId.get(id);
|
|
687
|
+
return {
|
|
688
|
+
id,
|
|
689
|
+
title: node?.title ?? null,
|
|
690
|
+
domain: node?.domain ?? null,
|
|
691
|
+
type,
|
|
692
|
+
direction,
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
export function uniqueDomains(graph) {
|
|
697
|
+
const set = new Set();
|
|
698
|
+
for (const n of graph.nodes) {
|
|
699
|
+
if (n.domain != null) set.add(n.domain);
|
|
700
|
+
}
|
|
701
|
+
return Array.from(set).sort();
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// --- repo-root discovery --------------------------------------------------
|
|
705
|
+
|
|
706
|
+
// `pa knowledge` needs an operator-repo root to find the graph. The
|
|
707
|
+
// existing pa CLI has `pa/repo-root.mjs`; import there rather than
|
|
708
|
+
// duplicate. This module keeps its dependency footprint to node stdlib +
|
|
709
|
+
// pa/repo-root.mjs.
|
|
710
|
+
export { getRepoRoot } from '../pa/repo-root.mjs';
|