diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb3398e9..f909eafc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,28 +40,30 @@ jobs: - name: Validate schemas run: | jq empty schema/*.json internal/testfixtures/oss-adapter-contract/*.json - jq empty harness/contracts/orient-bundle.schema.json + jq empty harness/contracts/portolan-bundle.schema.json + jq empty harness/contracts/landscape-card.schema.json + jq empty harness/contracts/landscape-report.schema.json - - name: Harness orient smoke - run: scripts/harness-orient-smoke.sh + - name: Harness portolan smoke + run: scripts/harness-portolan-smoke.sh - - name: Orient wizard orchestration smoke + - name: Portolan scan orchestration smoke run: | - scripts/orient-wizard.sh \ - internal/testfixtures/orient-bundle/target \ - /tmp/wizard-ci-orient \ + scripts/portolan-scan.sh \ + internal/testfixtures/portolan-bundle/target \ + /tmp/wizard-ci-portolan \ --no-viewer --skip-install --yes - test -f /tmp/wizard-ci-orient/manifest.json - test -s /tmp/wizard-ci-orient/gaps.jsonl - jq -e 'select(.status == "not_assessed")' /tmp/wizard-ci-orient/gaps.jsonl >/dev/null + test -f /tmp/wizard-ci-portolan/manifest.json + test -s /tmp/wizard-ci-portolan/gaps.jsonl + jq -e 'select(.status == "not_assessed")' /tmp/wizard-ci-portolan/gaps.jsonl >/dev/null jq -e 'select(.kind == "config" and .producer == "config-scan")' \ - /tmp/wizard-ci-orient/hotspots.jsonl >/dev/null - if ! jq -e 'select(.kind == "debt-candidate")' /tmp/wizard-ci-orient/hotspots.jsonl >/dev/null 2>&1; then + /tmp/wizard-ci-portolan/hotspots.jsonl >/dev/null + if ! jq -e 'select(.kind == "debt-candidate")' /tmp/wizard-ci-portolan/hotspots.jsonl >/dev/null 2>&1; then jq -e 'select(.id == "gap-ctags" or .surface == "symbols")' \ - /tmp/wizard-ci-orient/gaps.jsonl >/dev/null + /tmp/wizard-ci-portolan/gaps.jsonl >/dev/null fi - slug=$(printf '%s' "$(cd internal/testfixtures/orient-bundle/target && pwd)" | sha256sum | cut -c1-8) - test -n "$(find /tmp/wizard-ci-orient/producers/config -name "target-${slug}.jsonl" -print -quit)" + slug=$(printf '%s' "$(cd internal/testfixtures/portolan-bundle/target && pwd)" | sha256sum | cut -c1-8) + test -n "$(find /tmp/wizard-ci-portolan/producers/config -name "target-${slug}.jsonl" -print -quit)" - name: Diff whitespace check run: git diff --check diff --git a/.gitignore b/.gitignore index c7e96686..5f390d42 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ /bin/ /dist/ viewer/dist/ -**/orient-smoke/ +**/portolan-smoke/ .cursor/ /.portolan/ /coverage.out diff --git a/.specify/memory/constitution.md b/.specify/memory/constitution.md index 5b871d1f..e7e7e204 100644 --- a/.specify/memory/constitution.md +++ b/.specify/memory/constitution.md @@ -43,18 +43,18 @@ be verified through link, schema, and placeholder checks instead of code tests. ## Product Constraints - Primary delivery: harness artifacts (`harness/` skills, recipes, guardrails, - contracts) plus optional local orient viewer output. + contracts) plus optional local Portolan viewer output. - Implementation language: Go remains for the legacy CLI and normalization library; new product slices may use shell, TypeScript, or other local tooling when a spec documents the boundary. -- Runtime default: harness-first workflow (OSS recipes → orient bundle → viewer); +- Runtime default: harness-first workflow (OSS recipes → Portolan bundle → viewer); legacy Go CLI is frozen for new features until the Go decision gate resolves. -- Default output: ranked `orient/` hotspot bundle for navigation; legacy +- Default output: ranked Portolan bundle (`manifest.json`, `hotspots.jsonl`, …) for navigation; legacy machine-readable evidence graph remains supported as an optional bridge. -- Local viewer: a read-only static orient viewer may be served locally for the +- Local viewer: a read-only static Portolan viewer may be served locally for the duration of a user session; it must not mutate targets or require network by default. -- Viewer truth boundary: graph nodes in the orient viewer must come from imported +- Viewer truth boundary: graph nodes in the Portolan viewer must come from imported producer evidence or Portolan normalization; LLM-authored graphs are UX-only and must never be labeled `source-visible`. - Default privacy posture: no raw private source snippets, prompts, credentials, diff --git a/AGENTS.md b/AGENTS.md index 74cf4b88..91b3a525 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,7 +18,7 @@ Portolan is not: Portolan is: - a read-only local discovery substrate an agent can run; -- a harness-first supplement (`harness/SKILL.md`, recipes, guardrails, orient +- a harness-first supplement (`harness/SKILL.md`, recipes, guardrails, Portolan viewer) with an optional legacy Go CLI bridge; - a normalizer for source, metadata, runtime, and claim evidence; - a machine-readable evidence graph; @@ -42,7 +42,7 @@ Portolan is: ## Engineering Rules -- Primary delivery: harness artifacts and orient bundle contract; see spec 087. +- Primary delivery: harness artifacts and Portolan bundle contract; see spec 087. - Go CLI is frozen for new features per [`docs/harness/GO-FREEZE-POLICY.md`](docs/harness/GO-FREEZE-POLICY.md). - Keep `cmd/portolan` thin; put behavior in internal packages when Go changes are @@ -193,9 +193,11 @@ Run: go test ./... go vet ./... jq empty schema/*.json -jq empty harness/contracts/orient-bundle.schema.json -scripts/harness-orient-smoke.sh -scripts/orient-wizard.sh --help +jq empty harness/contracts/portolan-bundle.schema.json +scripts/harness-portolan-smoke.sh +scripts/portolan-scan.sh --help +jq empty harness/contracts/landscape-card.schema.json +jq empty harness/contracts/landscape-report.schema.json git diff --check ``` @@ -214,11 +216,17 @@ shell commands, and other important information, read the current plan: ## Learned User Preferences - Ask clarifying questions before drafting implementation plans when harness targets, Go role, or MVP surface are still open. -- Prioritize user-facing landscape navigation (hotspots, tech debt, duplication, where to look first) over evidence-discipline artifacts as the primary deliverable. +- Prioritize a report-first landscape viewer (Overview landing with ranked problems) over raw hotspot lists, map-only icicles, or evidence-discipline artifacts as the primary deliverable. - Treat B2B evidence guardrails as a secondary layer on top of the map, not the main reason to use Portolan. - Expand harness rules, guardrails, and OSS tool recipes rather than the Go codebase when adding product behavior. +- Use Portolan-prefixed names in user-facing CLI, scripts, and UI copy (e.g. `portolan-scan`, `scripts/portolan-scan.sh`). +- Explain in the viewer why findings rank as problems and what each navigation control does. +- Review delivery by observable facts and demo behavior, not code inspection alone. ## Learned Workspace Facts -- Understand-Anything is the reference UX for the local orient map; graph nodes must stay evidence-backed, not LLM-authored truth. +- Understand-Anything drill-down inside a landscape report is the reference UX; graph nodes must stay evidence-backed, not LLM-authored truth. - Product-success questions center on concrete code pain (debt, duplication, risky zones), not abstract trust infrastructure alone. +- Landscape report UX references Portolan `map.md` sections and the standalone `landscape-card.json` contract; no sdp_lab runtime dependency. +- Demo-ready output must let an operator identify the target, repos, top problems, and gaps without external docs. +- Scan and viewer inventories must honor `.gitignore`. diff --git a/README.md b/README.md index 950a76e1..3fd49f84 100644 --- a/README.md +++ b/README.md @@ -34,22 +34,22 @@ partly black-box. ## Harness-First Quick Start (recommended) Portolan is primarily a **harness supplement**: portable skill, OSS recipes, -guardrails, and a local orient map — not a Go module you must install first. +guardrails, and a local hotspot viewer — not a Go module you must install first. **One command** (tool check → recipes → bundle → viewer): ```bash -scripts/orient-wizard.sh --yes +scripts/portolan-scan.sh --yes ``` -Add `--no-viewer` to build only. See `scripts/orient-wizard.sh --help`. +Add `--no-viewer` to build only. See `scripts/portolan-scan.sh --help`. -The orient viewer supports search, filters (including `config` and `debt-candidate` -kinds), a directory heat map, and click-to-source preview (local files only). +The viewer shows a ranked hotspot list, folder tree, search, filters (including +`config` and `debt-candidate` kinds), and click-to-source preview (local files only). See [`docs/demo-runbook.md`](docs/demo-runbook.md). Manual fallback: read [`harness/SKILL.md`](harness/SKILL.md), run recipes from -[`harness/recipes/`](harness/recipes/), then `scripts/build-orient-bundle.sh`. +[`harness/recipes/`](harness/recipes/), then `scripts/build-portolan-bundle.sh`. See [`docs/harness/GO-FREEZE-POLICY.md`](docs/harness/GO-FREEZE-POLICY.md) for legacy Go CLI status. @@ -300,7 +300,7 @@ For repository development: ```bash go test -count=1 ./... jq empty schema/*.json -jq empty harness/contracts/orient-bundle.schema.json -scripts/harness-orient-smoke.sh +jq empty harness/contracts/portolan-bundle.schema.json +scripts/harness-portolan-smoke.sh git diff --check ``` diff --git a/docs/adr/001-go-cli-fate.md b/docs/adr/001-go-cli-fate.md index 8a45f836..68477f99 100644 --- a/docs/adr/001-go-cli-fate.md +++ b/docs/adr/001-go-cli-fate.md @@ -2,15 +2,15 @@ **Status**: Provisional — thin maintenance layer (2026-06-10) -**Context**: Harness pivot (spec 087) ships recipes, orient bundle, and viewer +**Context**: Harness pivot (spec 087) ships recipes, Portolan bundle, and viewer without requiring Go. Phase 5 smoke validates harness-only path on fixtures. ## Decision gate criteria | Criterion | Result (2026-06-10 smoke) | | --- | --- | -| Harness + scripts build orient bundle | Pass (`build-orient-bundle.sh`) | -| Viewer loads bundle without Go | Pass (`harness-orient-smoke.sh`) | +| Harness + scripts build Portolan bundle | Pass (`build-portolan-bundle.sh`) | +| Viewer loads bundle without Go | Pass (`harness-portolan-smoke.sh`) | | Importer edge cases in production | Not re-tested; legacy `internal/importer` retained | | Large JSONL / path safety | Legacy Go still has tests; harness uses jq/bash | @@ -21,7 +21,7 @@ cases or a dedicated spec deprecates `cmd/portolan`. Allowed: -- `map`, `import`, `query`, `orient-export-from-map.sh` bridge +- `map`, `import`, `query`, `portolan-export-from-map.sh` bridge - Bugfixes and security fixes per GO-FREEZE-POLICY Not allowed without new ADR: diff --git a/docs/agent/INSTALL-PROMPT.md b/docs/agent/INSTALL-PROMPT.md index 1270cb0a..794d3d29 100644 --- a/docs/agent/INSTALL-PROMPT.md +++ b/docs/agent/INSTALL-PROMPT.md @@ -14,29 +14,29 @@ Replace variables with absolute local paths: ```text PORTOLAN_PATH= TARGET_PATH= -ORIENT_PATH= +BUNDLE_DIR= ``` Then send: ```text -Run the Portolan orient harness on TARGET_PATH. Write the orient bundle to -ORIENT_PATH. Execute now; do not ask unless a path is missing. +Run the Portolan harness on TARGET_PATH. Write the Portolan bundle to +BUNDLE_DIR. Execute now; do not ask unless a path is missing. 1. Read PORTOLAN_PATH/harness/SKILL.md -2. PORTOLAN_PATH/scripts/orient-wizard.sh TARGET_PATH ORIENT_PATH --no-viewer --yes - (or run recipes manually + build-orient-bundle.sh if operator prefers) +2. PORTOLAN_PATH/scripts/portolan-scan.sh TARGET_PATH BUNDLE_DIR --no-viewer --yes + (or run recipes manually + build-portolan-bundle.sh if operator prefers) 3. Read hotspots.jsonl and gaps.jsonl; cite hotspot.id and producer_ref per PORTOLAN_PATH/harness/guardrails/citation-rules.md If harness blocks external writes, use -PORTOLAN_PATH/.portolan/runs/ as ORIENT_PATH. +PORTOLAN_PATH/.portolan/runs/ as BUNDLE_DIR. Answer with: 1. Recipes run (verified/failed/blocked) 2. Top 5 hotspots by rank with evidence 3. Gaps and not_assessed surfaces -4. Viewer command: cd PORTOLAN_PATH/viewer && npm run serve -- --bundle ORIENT_PATH +4. Viewer command: cd PORTOLAN_PATH/viewer && npm run serve -- --bundle BUNDLE_DIR 5. Unsupported claims avoided Legacy Go CLI (optional): docs/harness/GO-FREEZE-POLICY.md diff --git a/docs/demo-runbook.md b/docs/demo-runbook.md index 503fec8e..5ec58f81 100644 --- a/docs/demo-runbook.md +++ b/docs/demo-runbook.md @@ -1,52 +1,66 @@ # Portolan Demo Runbook -Live demo for a newcomer: **where code pain is** → **filter** → **directory tree** → **source**. +Live demo for a newcomer: **landscape report in 10 seconds** → **findings by section** → **gaps** → **drill-down to source**. -## Setup (two commands) +## Setup **Single repo (portolan):** ```bash -scripts/orient-wizard.sh . /tmp/orient-portolan --yes +scripts/portolan-scan.sh . /tmp/portolan-portolan --yes ``` **Bounded multi-repo (bigtop quick sample):** ```bash -scripts/orient-wizard.sh ~/projects/bigtop-landscape/repos /tmp/orient-bigtop \ +scripts/portolan-scan.sh ~/projects/bigtop-landscape/repos /tmp/portolan-bigtop \ --yes --limit-repos 3 --producers semgrep,syft ``` **Full landscape stress (18 repos, spec 091):** ```bash -scripts/orient-wizard.sh ~/projects/bigtop-landscape/repos /tmp/orient-bigtop-full \ +scripts/portolan-scan.sh ~/projects/bigtop-landscape/repos /tmp/portolan-bigtop-full \ --no-viewer --yes --shard-timeout 600 --jscpd-memory-mb 2048 ``` -Expect 30–90+ minutes. Failed shards appear in gaps (not a wizard abort). -Use viewer on the result: `node viewer/scripts/serve.js --bundle /tmp/orient-bigtop-full` +Expect 30–90+ minutes. Failed shards appear in gaps (not a scan abort). -Wizard opens viewer automatically. For a fixed port: +`portolan-scan` opens the viewer automatically. For a fixed port: ```bash cd viewer && node scripts/build-static.js -node scripts/serve.js --bundle /tmp/orient-portolan --port 4173 +node scripts/serve.js --bundle /tmp/portolan-portolan --port 4173 ``` Open http://127.0.0.1:4173/ -## 5-step demo script +## 5-step demo script (report-first) -1. **Context** — Point at header: target path, hotspot count, evidence-backed (not LLM graph). -2. **Search** — Type `TODO` or `duplicate`; list and tree narrow instantly. -3. **Filter** — Toggle `static-finding`, `duplication`, or **`config`** (Docker/CI/env inventory); on bigtop, pick a **Repo** chip. Optional: `debt-candidate` for symbol-dense files when ctags ran. -4. **Directory heat map** — Expand a hot folder (severity bar + count); click a hotspot row. -5. **Source** — In Detail, show file paths and the **Source** snippet from local files (read-only). +1. **Overview tab (default)** — Project card: language, scale, maturity (README/CI/tests/Docker), repo matrix, findings shown vs scan total, top “where to look first”. +2. **Gaps tab** — What was **not** assessed (`not_assessed` / `cannot_verify`); not hidden findings. +3. **Findings tab** — Sections by kind (map.md parity), then folder tree + ranked list. +4. **Search & filter** — Type `TODO` or `duplicate`; use views (Tour top 15, Code pain, Config). +5. **Drill-down** — Click a finding → detail + **Source** snippet (read-only local files). + +If bundle is truncated, use **Show all findings from scan** on the Findings tab. + +## Demo bar (acceptance) + +Operator answers without external docs: + +| Question | Where in viewer | +| --- | --- | +| What is this target? | Overview → landscape card | +| How many repos? | Overview → repo table | +| Top problems? | Overview → next steps; Findings → sections | +| What was not checked? | Gaps tab | + +Compare with `docs/test-corpora/apache-bigtop/examples/map-excerpt.md` for section parity. ## Talking points -- Gaps/truncation banner = honest limits (`not_assessed`, budget cap). +- Gaps/truncation = honest limits (`not_assessed`, budget cap). - Every claim ties to `producer_ref` (jscpd, semgrep, syft, config-scan, ctags). - No network; local-first. diff --git a/docs/harness/GO-FREEZE-POLICY.md b/docs/harness/GO-FREEZE-POLICY.md index 6f753dd7..f90e93cf 100644 --- a/docs/harness/GO-FREEZE-POLICY.md +++ b/docs/harness/GO-FREEZE-POLICY.md @@ -13,7 +13,7 @@ features** until the Go decision gate in - Bug fixes with regression tests. - Security fixes for path handling and untrusted artifact imports. - Documentation corrections. -- Bridge commands/scripts that export legacy map output to `orient/` bundles. +- Bridge commands/scripts that export legacy map output to Portolan bundles. - Dependency patches required for `go test ./...` on supported Go versions. ## Not allowed without a new spec and Go gate outcome @@ -28,14 +28,14 @@ features** until the Go decision gate in Use [`harness/SKILL.md`](../../harness/SKILL.md): 1. Run OSS recipes from `harness/recipes/`. -2. Build `orient/` with `scripts/build-orient-bundle.sh`. +2. Build a Portolan bundle with `scripts/build-portolan-bundle.sh`. 3. Open the local viewer from `viewer/`. Optional legacy bridge: ```bash go run ./cmd/portolan map --root --out -scripts/orient-export-from-map.sh +scripts/portolan-export-from-map.sh ``` ## Review diff --git a/docs/harness/tool-profiles/jscpd.md b/docs/harness/tool-profiles/jscpd.md index c9bc25dc..1dec0e2f 100644 --- a/docs/harness/tool-profiles/jscpd.md +++ b/docs/harness/tool-profiles/jscpd.md @@ -6,7 +6,7 @@ | User job | Code duplication / near-clone clusters | | License | MIT | | Review date | 2026-06-10 | -| Portolan action | Import JSON output into `orient/hotspots.jsonl` (`kind: duplication`) | +| Portolan action | Import JSON output into `/hotspots.jsonl` (`kind: duplication`) | ## Output surface diff --git a/docs/harness/tool-profiles/understand-anything.md b/docs/harness/tool-profiles/understand-anything.md index 10728257..5c26081c 100644 --- a/docs/harness/tool-profiles/understand-anything.md +++ b/docs/harness/tool-profiles/understand-anything.md @@ -11,7 +11,7 @@ ## Output surface - Upstream: interactive graph + Q&A (not Portolan evidence) -- Portolan: `orient/` bundle consumed by `viewer/` +- Portolan: Portolan bundle consumed by `viewer/` ## Risks diff --git a/docs/onboarding.md b/docs/onboarding.md index 9ad088c5..a9ec5102 100644 --- a/docs/onboarding.md +++ b/docs/onboarding.md @@ -9,8 +9,8 @@ first. | Intent | Start with | Then read | Boundary to preserve | | --- | --- | --- | --- | | Understand what Portolan is for | [README](../README.md) | [Product Claims](product-claims.md), [Product Boundary](product-boundary.md), [Product Quality Boundary](product-quality-boundary.md) | Portolan is a local evidence layer, not a coding harness, readiness gate, service catalog, observability platform, or Cursor/OpenCode replacement. | -| Ask an agent to inspect a local target | [Harness Skill](../harness/SKILL.md) | [Install Prompt](agent/INSTALL-PROMPT.md), [Agent Acceptance](agent/ACCEPTANCE.md) | Harness-first: recipes → orient bundle → viewer. Cite hotspot.id and preserve gaps. | -| Open the orient map (human) | [Viewer README](../viewer/README.md) | [Harness Skill](../harness/SKILL.md) | Local read-only viewer; evidence from producers only. | +| Ask an agent to inspect a local target | [Harness Skill](../harness/SKILL.md) | [Install Prompt](agent/INSTALL-PROMPT.md), [Agent Acceptance](agent/ACCEPTANCE.md) | Harness-first: recipes → bundle → viewer. Cite hotspot.id and preserve gaps. | +| Open the viewer (human) | [Viewer README](../viewer/README.md) | [Harness Skill](../harness/SKILL.md) | Local read-only viewer; folder tree + ranked hotspots from producers only. | | Install or resolve the command | [Agent Install](agent/INSTALL.md) | [Troubleshooting](agent/TROUBLESHOOTING.md), [Release Guide](release.md) | Source bootstrap is local and does not fetch Go modules unless explicitly approved. | | Use Cursor | [Agent Install Prompt](agent/INSTALL-PROMPT.md) | [Agent Acceptance](agent/ACCEPTANCE.md), [Product Claims](product-claims.md) | Current verified Cursor evidence is headless Cursor Agent CLI / Composer. Cursor UI behavior is outside the current required acceptance scope; no root-level Cursor rule is shipped. | | Use OpenCode | [Install Prompt](agent/INSTALL-PROMPT.md) | [Agent Acceptance](agent/ACCEPTANCE.md), [Product Claims](product-claims.md) | OpenCode default-permission runs are verified with repo-local output under the Portolan checkout. The recorded external-output default-permission lane failed. | diff --git a/docs/product-backlog.md b/docs/product-backlog.md index bca00b74..10c95fe0 100644 --- a/docs/product-backlog.md +++ b/docs/product-backlog.md @@ -157,7 +157,7 @@ fixtures are preflight evidence only. | P6-073 | `docs/specs/073-bigtop-runtime-capture-execution/` | The explicitly approved single-node Bigtop Docker provisioner create/capture/destroy run is executed and ledgered with runtime-visible service evidence, cleanup evidence, and claim boundaries. | Merged via PR #51; approved runtime create/capture/destroy executed locally. Container, Docker network, inspect output, and one running YARN NodeManager were verified as `runtime-visible`; NameNode, ResourceManager, HistoryServer, and ProxyServer failed, Datanode was not installed/skipped, cleanup removed the container/network and target repo residue; Cursor stress, three assessed non-GPT review lanes, local baseline, GitHub checks, squash merge, and branch cleanup verified; full Bigtop runtime topology and enterprise parity remain `cannot_verify`; GitHub review approval remains `not_assessed` | | P6-074 | `docs/specs/074-bigtop-runtime-topology-health-capture/` | The partial Bigtop runtime capture is converted into a health-oriented topology proof attempt with explicit per-service assertions, daemon logs, smoke probes, cleanup, and failure classification. | Merged via PR #52; depends on P6-073. Planning, approval packet, health command contract, runtime summary schema, Cursor scope stress, three assessed non-GPT review lanes, local baseline, GitHub checks, squash merge, and branch cleanup verified. Runtime execution remains blocked pending fresh explicit approval for the named 074 command sequence; complete topology remains `cannot_verify` until this slice produces service-health and smoke-probe evidence | | P6-075 | `docs/specs/075-bigtop-producer-output-coverage-closure/` | Real Bigtop symbol/API/catalog/model/runtime producer outputs beyond Syft/CycloneDX are inventoried, refreshed, normalized or ledgered, and coverage-scored against the architecture rubric. | Merged via PR #53; producer-output breadth beyond Syft/CycloneDX is verified only as confirmed bounded outputs with source ledgers, Cursor stress, three assessed non-GPT review lanes, local baseline, GitHub checks, squash merge, and remote branch cleanup verified. Complete runtime topology, full symbol/reference graph, call graph, and human/enterprise parity remain `cannot_verify`; follow-up specs 074, 077, and 076 own those gaps | -| P6-076 | `docs/specs/076-cursor-enterprise-parity-validation/` | Cursor plus Portolan is re-evaluated against the Bigtop human/enterprise architecture rubric after runtime and producer-output closure, with claim promotion allowed only for criteria proven by current evidence. | Planning gate merged via PR #55; **frozen from P7 harness MVP path** until orient harness ships on real targets. Default paired Cursor stress blocked until P6-074 runtime-health evidence exists | +| P6-076 | `docs/specs/076-cursor-enterprise-parity-validation/` | Cursor plus Portolan is re-evaluated against the Bigtop human/enterprise architecture rubric after runtime and producer-output closure, with claim promotion allowed only for criteria proven by current evidence. | Planning gate merged via PR #55; **frozen from P7 harness MVP path** until Portolan harness ships on real targets. Default paired Cursor stress blocked until P6-074 runtime-health evidence exists | | P6-077 | `docs/specs/077-bigtop-callgraph-symbol-closure/` | Full symbol/reference/call graph closure is attempted with mature local producers or explicitly classified as `cannot_verify` with reviewed rationale. | Merged via PR #54; depends on P6-075. Producer decision record, read-only availability ledger, Cursor Composer 2.5 claim-boundary stress, three assessed non-GPT review lanes, local baseline, PR creation, GitHub checks, explicit user merge approval, squash merge, and remote branch cleanup verified; no safe full resolved graph producer is currently available, so full C6 and call-graph parity remain `cannot_verify`; GitHub review approval remains `not_assessed` | | P6-078 | `docs/specs/078-build-tool-dependency-producers/` | Context packs recommend approval-gated native Maven/Gradle dependency producer outputs when build manifests are visible, so Java/Scala/Maven relationship gaps get concrete local evidence steps without Portolan owning per-language scanners. | Merged via PR #56; local baseline, fresh Bigtop context smoke, three assessed non-GPT review lanes, GitHub checks, explicit user merge approval, squash merge, and remote branch cleanup verified. No Maven/Gradle command was executed by default, no network/install/build was approved, and dependency evidence remains `not_assessed` until local CycloneDX/build-tool output is supplied; GitHub review approval remains `not_assessed` | | P6-079 | `docs/specs/079-jscpd-sharded-duplication-plan/` | Context packs recommend repository-sharded jscpd output recipes for large multi-repo landscapes so duplication evidence can be acquired incrementally without treating full-root jscpd OOM as evidence. | Merged via PR #57; local baseline, fresh Bigtop context smoke, Cursor Composer 2.5 sharded-plan stress, three assessed non-GPT review lanes, GitHub checks, explicit user merge approval, squash merge `f4a4951`, and remote branch cleanup verified. Scope is `oss-plan.json` guidance and focused tests only; Portolan did not execute jscpd, did not install stores/plugins, and duplication evidence remains `not_assessed` until local jscpd output exists; GitHub review approval remains `not_assessed` | @@ -167,23 +167,24 @@ fixtures are preflight evidence only. | P6-083 | `docs/specs/083-tool-acquisition-guidance/` | Context packs make tool acquisition guidance explicit and stack-agnostic: agents can pull in the right local producer tools without treating Portolan as a PHP/JVM/Gradle adapter stack. | Merged via PR #61; local baseline, fresh Bigtop context smoke, Cursor Composer 2.5 stress, integrated PR #57-#61 stack-agnostic stress, three assessed non-GPT review lanes, GitHub checks, explicit user merge approval, squash merge `847e84e`, post-merge Bigtop context smoke, and remote branch cleanup verified. Native producer execution, tool install/acquisition, component inventory, dependency relationships, duplication metrics, runtime topology, and GitHub review approval remain `not_assessed` | | P6-084 | `docs/specs/084-external-tool-evaluation-profiles/` | Portolan keeps dated evaluation profiles for CodeGraph, Understand-Anything, and ast-index so external tool adoption stays explicit, evidence-honest, and refreshable. | Implemented under P7-084; profiles in `docs/harness/tool-profiles/` | | P6-085 | `docs/specs/085-ast-index-producer-import/` | Explicit local ast-index outputs can be imported as bounded symbol/reference/module producer evidence without Portolan installing, executing, watching, or mutating targets. | Draft spec; backlog-only. No plan/tasks yet. ast-index execution, real output acquisition, importer implementation, CodeGraph import, and call-graph parity remain `not_assessed` | -| P6-086 | `docs/specs/086-evidence-navigation-ux-patterns/` | Useful navigation ideas from Understand-Anything, CodeGraph, and ast-index are adopted as evidence-backed guide patterns without accepting LLM-authored graphs or live dashboards as truth. | MVP viewer in `viewer/`; harness-orient-smoke verified; full UA fork deferred | +| P6-086 | `docs/specs/086-evidence-navigation-ux-patterns/` | Useful navigation ideas from Understand-Anything, CodeGraph, and ast-index are adopted as evidence-backed guide patterns without accepting LLM-authored graphs or live dashboards as truth. | MVP viewer in `viewer/`; harness-portolan-smoke verified; full UA fork deferred | -## P7: Harness-First Orient (active) +## P7: Harness-First Portolan (active) | ID | Spec | Outcome | Status | | --- | --- | --- | --- | | P7-087 | `docs/specs/087-harness-first-product/` | Product narrative, constitution 1.1.0, Go freeze policy, harness-first README route. | Implemented (harness pivot slice) | | P7-084 | `docs/specs/084-external-tool-evaluation-profiles/` | Dated tool profiles and first-wave recipes for jscpd, Semgrep, Syft, ctags, UA, CodeGraph, ast-index. | Implemented (profiles + recipes) | -| P7-088 | `docs/specs/088-orient-bundle-contract/` | `orient/` bundle contract, build script, map bridge. | Implemented | -| P7-089 | `docs/specs/089-orient-wizard/` | One-command `orient-wizard.sh`: tool check, consent install, recipes, bundle, viewer; real-target smoke. | Implemented | +| P7-088 | `docs/specs/088-portolan-bundle-contract/` | Portolan bundle contract, build script, map bridge. | Implemented | +| P7-089 | `docs/specs/089-portolan-scan/` | One-command scan orchestration (now `portolan-scan.sh`): tool check, consent install, recipes, bundle, viewer. | Implemented (renamed in 093) | | P7-090 | `docs/specs/090-viewer-demo-ux/` | Demo-ready viewer: search, filters, directory heat map, source preview, demo runbook. | Implemented | | P7-091 | `docs/specs/091-landscape-scale/` | Full bigtop scale: bounded jscpd shards, shard gaps, kind-quota budget, hotspots-full.jsonl. | Implemented | | P7-092 | `docs/specs/092-orient-surfaces/` | Config-surface inventory + ctags symbol-density producers; PR #64 debt closure; kind-quota refresh; gitignore hardening. | Implemented via PR #65 | -| P7-086 | `docs/specs/086-evidence-navigation-ux-patterns/` | Local orient viewer (UA-inspired) over evidence hotspots. | Demo-ready in `viewer/` (spec 090) | +| P7-093 | `docs/specs/093-viewer-landscape-ux/` | Landscape report viewer: `portolan-scan`, landscape-card/report bundle, tabbed Overview/Findings/Gaps (report-first, map.md sections). | Ready for review (PR #66) | +| P7-086 | `docs/specs/086-evidence-navigation-ux-patterns/` | Local Portolan viewer (UA-inspired) over evidence hotspots. | Demo-ready in `viewer/` (spec 090) | | P7-ADR | `docs/adr/001-go-cli-fate.md` | Go CLI maintenance-mode decision after harness smoke. | Provisional ADR recorded | -**MVP path:** `scripts/orient-wizard.sh` (or `harness/SKILL.md` → recipes → `build-orient-bundle.sh`) → `viewer/`. +**MVP path:** `scripts/portolan-scan.sh` (or `harness/SKILL.md` → recipes → `build-portolan-bundle.sh`) → `viewer/`. **Frozen from P7 MVP:** Bigtop enterprise parity (076), runtime capture chain (061–065), new Go `contextprep` features. diff --git a/docs/product-claims.md b/docs/product-claims.md index dffc6bb0..8df81ab9 100644 --- a/docs/product-claims.md +++ b/docs/product-claims.md @@ -16,16 +16,16 @@ surface derived from that evidence. Portolan helps engineers and agents find where local code pain is visible: duplication clusters, static findings, dependency hubs, and configuration -hotspots — then navigate there through a harness workflow and local orient map. +hotspots — then navigate there through a harness workflow and the local viewer. The primary product path is harness-first: follow [`harness/SKILL.md`](../harness/SKILL.md), -run documented OSS recipes, build an `orient/` bundle, and open the local viewer. +run documented OSS recipes, build an Portolan bundle, and open the local viewer. The legacy Go CLI (`context prepare`, `map`) remains an optional bridge. Evidence discipline is a **secondary** (B2B) value: hotspots and viewer nodes cite `producer_ref` and evidence states; unknowns stay visible without blocking first-run navigation. Portolan does not replace mature scanners — it composes -their local outputs into one orient surface. +their local outputs into one Portolan surface. Named examples, including the Apache Bigtop runs, are evidence records for specific target shapes. They are not the main product path and must not be diff --git a/docs/research/2026-06-10-understand-anything-fork-spike.md b/docs/research/2026-06-10-understand-anything-fork-spike.md index 7e49322f..bc09baac 100644 --- a/docs/research/2026-06-10-understand-anything-fork-spike.md +++ b/docs/research/2026-06-10-understand-anything-fork-spike.md @@ -26,7 +26,7 @@ summarization — **unacceptable as Portolan `source-visible` evidence**. 1. **Do not** ingest UA LLM graph output as evidence. 2. **Do** implement `viewer/` as a UA-inspired local viewer that loads only - `orient/manifest.json` + `orient/hotspots.jsonl` + optional `graph-slice.json`. + `manifest.json` + `hotspots.jsonl` + optional `graph-slice.json`. 3. **Future option**: submodule or vendor UA frontend after stripping LLM graph generation; replace data loader with `loadPortolanBundle()`. @@ -37,7 +37,7 @@ summarization — **unacceptable as Portolan `source-visible` evidence**. Rationale: - Harness skill references one checkout path. -- Orient bundle contract stays co-located with `harness/contracts/`. +- Portolan bundle contract stays co-located with `harness/contracts/`. - Sibling repo deferred until viewer needs independent release cadence. ## Effort estimate @@ -64,5 +64,5 @@ Rationale: ## Verification -MVP done when `viewer/` opens a fixture `orient/` bundle and shows ranked hotspots +MVP done when `viewer/` opens a fixture Portolan bundle and shows ranked hotspots with evidence badges without network access. diff --git a/docs/specs/086-evidence-navigation-ux-patterns/plan.md b/docs/specs/086-evidence-navigation-ux-patterns/plan.md index 2e6e2ff0..3f6389aa 100644 --- a/docs/specs/086-evidence-navigation-ux-patterns/plan.md +++ b/docs/specs/086-evidence-navigation-ux-patterns/plan.md @@ -4,7 +4,7 @@ ## Summary -Ship UA-inspired navigation as `viewer/` loading orient bundle evidence only. +Ship UA-inspired navigation as `viewer/` loading Portolan bundle evidence only. Constitution 1.1.0 allows local static viewer session. ## Deliverables @@ -15,5 +15,5 @@ Constitution 1.1.0 allows local static viewer session. ## Verification ```bash -scripts/harness-orient-smoke.sh +scripts/harness-portolan-smoke.sh ``` diff --git a/docs/specs/086-evidence-navigation-ux-patterns/tasks.md b/docs/specs/086-evidence-navigation-ux-patterns/tasks.md index 7ce3df50..6e8bdc43 100644 --- a/docs/specs/086-evidence-navigation-ux-patterns/tasks.md +++ b/docs/specs/086-evidence-navigation-ux-patterns/tasks.md @@ -3,4 +3,4 @@ - [x] T001 Revise FR-005 for local static viewer (constitution 1.1.0). - [x] T002 Implement viewer/ MVP with Portolan bundle adapter. - [x] T003 Wire harness SKILL to viewer serve command. -- [x] T004 harness-orient-smoke.sh verification. +- [x] T004 harness-portolan-smoke.sh verification. diff --git a/docs/specs/087-harness-first-product/plan.md b/docs/specs/087-harness-first-product/plan.md index 2c3b84f7..6af7643a 100644 --- a/docs/specs/087-harness-first-product/plan.md +++ b/docs/specs/087-harness-first-product/plan.md @@ -4,16 +4,16 @@ ## Summary -Reposition Portolan around harness artifacts, OSS recipes, orient bundle contract, +Reposition Portolan around harness artifacts, OSS recipes, Portolan bundle contract, and a local evidence-backed viewer. Publish governance (constitution amendment, Go freeze policy) before expanding harness and viewer slices. ## Technical Context **Primary delivery**: `harness/` skills, recipes, guardrails, contracts; `viewer/` -local orient UI; `docs/adr/` decision records. +local Portolan UI; `docs/adr/` decision records. -**Legacy**: Go CLI frozen; optional `scripts/orient-export-from-map.sh` bridge. +**Legacy**: Go CLI frozen; optional `scripts/portolan-export-from-map.sh` bridge. **Constraints**: Local-first, read-only default; viewer session may use local static serve only. @@ -32,6 +32,6 @@ serve only. ```bash test -f harness/SKILL.md test -f docs/harness/GO-FREEZE-POLICY.md -jq empty harness/contracts/orient-bundle.schema.json +jq empty harness/contracts/portolan-bundle.schema.json git diff --check ``` diff --git a/docs/specs/087-harness-first-product/spec.md b/docs/specs/087-harness-first-product/spec.md index 3f68d13c..8958273b 100644 --- a/docs/specs/087-harness-first-product/spec.md +++ b/docs/specs/087-harness-first-product/spec.md @@ -7,7 +7,7 @@ **Status**: Active implementation slice for the Portolan harness pivot. **Input**: Product pivot — Portolan is a harness supplement (rules, recipes, -guardrails, OSS assembly) with an orient map viewer, not primarily a Go module. +guardrails, OSS assembly) with a Portolan map viewer, not primarily a Go module. B2B evidence guardrails are a cherry on top, not the hero value. ## User Scenarios & Testing @@ -16,20 +16,20 @@ B2B evidence guardrails are a cherry on top, not the hero value. An engineer or agent asks where duplication, static smells, dependency hubs, or config risks are visible locally and receives a ranked hotspot list plus an -interactive orient map. +interactive Portolan map. **Why this priority**: This is the primary user job. Evidence discipline supports the answer; it does not replace the answer. **Independent Test**: A fixture target with jscpd and Semgrep outputs produces -an `orient/` bundle and the viewer shows at least one duplication and one +an Portolan bundle and the viewer shows at least one duplication and one static-finding hotspot with file paths. **Acceptance Scenarios**: 1. **Given** local producer outputs exist, **When** the harness workflow runs, - **Then** `orient/hotspots.jsonl` lists ranked pain points with `producer_ref`. -2. **Given** jscpd output is missing, **When** the orient bundle is built, + **Then** `/hotspots.jsonl` lists ranked pain points with `producer_ref`. +2. **Given** jscpd output is missing, **When** the Portolan bundle is built, **Then** duplication remains `not_assessed` and the viewer shows a gap badge, not synthetic clone clusters. @@ -38,11 +38,11 @@ static-finding hotspot with file paths. ### User Story 2 - Harness-Independent Agent Workflow (Priority: P1) An agent in Cursor, OpenCode, Codex, or Claude can follow one portable skill to -run OSS recipes, build the orient bundle, and open the local viewer without +run OSS recipes, build the Portolan bundle, and open the local viewer without bootstrapping the Go CLI. **Independent Test**: `harness/SKILL.md` completes on a fixture repo using only -documented recipes and `scripts/build-orient-bundle.sh`. +documented recipes and `scripts/build-portolan-bundle.sh`. --- @@ -57,7 +57,7 @@ viewer badges match evidence states. ## Requirements - **FR-001**: The primary product path MUST be harness install → OSS recipes → - `orient/` bundle → local viewer. + Portolan bundle → local viewer. - **FR-002**: The hero product claim MUST be navigation to code pain (duplication, static findings, deps/config hotspots), not evidence discipline alone. - **FR-003**: Portable harness artifacts MUST work across Cursor, OpenCode, and diff --git a/docs/specs/087-harness-first-product/tasks.md b/docs/specs/087-harness-first-product/tasks.md index d226a78d..55e2468f 100644 --- a/docs/specs/087-harness-first-product/tasks.md +++ b/docs/specs/087-harness-first-product/tasks.md @@ -9,4 +9,4 @@ ## Dependencies -- Blocks harness recipes (084), orient bundle (088), viewer (086 revised). +- Blocks harness recipes (084), Portolan bundle (088), viewer (086 revised). diff --git a/docs/specs/088-orient-bundle-contract/plan.md b/docs/specs/088-orient-bundle-contract/plan.md index 098056b8..9cdb7ba5 100644 --- a/docs/specs/088-orient-bundle-contract/plan.md +++ b/docs/specs/088-orient-bundle-contract/plan.md @@ -2,14 +2,14 @@ ## Deliverables -- `harness/contracts/orient-bundle.schema.json` -- `scripts/build-orient-bundle.sh` -- `scripts/orient-export-from-map.sh` -- `internal/testfixtures/orient-bundle/` fixture +- `harness/contracts/portolan-bundle.schema.json` +- `scripts/build-portolan-bundle.sh` +- `scripts/portolan-export-from-map.sh` +- `internal/testfixtures/portolan-bundle/` fixture ## Verification ```bash -scripts/build-orient-bundle.sh internal/testfixtures/orient-bundle/target internal/testfixtures/orient-bundle/orient -jq empty harness/contracts/orient-bundle.schema.json +scripts/build-portolan-bundle.sh internal/testfixtures/portolan-bundle/target internal/testfixtures/portolan-bundle/reference +jq empty harness/contracts/portolan-bundle.schema.json ``` diff --git a/docs/specs/088-orient-bundle-contract/spec.md b/docs/specs/088-orient-bundle-contract/spec.md index dba78171..aabc286c 100644 --- a/docs/specs/088-orient-bundle-contract/spec.md +++ b/docs/specs/088-orient-bundle-contract/spec.md @@ -1,6 +1,6 @@ -# Feature Specification: Orient Bundle Contract +# Feature Specification: Portolan Bundle Contract -**Feature Branch**: `codex/088-orient-bundle-contract` +**Feature Branch**: `codex/088-portolan-bundle-contract` **Created**: 2026-06-10 @@ -8,18 +8,18 @@ ## User Scenarios -### User Story 1 - Single Orient Directory (Priority: P1) +### User Story 1 - Single Portolan Bundle Directory (Priority: P1) -Harness recipes and `scripts/build-orient-bundle.sh` produce one `orient/` +Harness recipes and `scripts/build-portolan-bundle.sh` produce one Portolan directory that both the viewer and agents consume. **Independent Test**: Fixture producers → bundle validates against -`harness/contracts/orient-bundle.schema.json`. +`harness/contracts/portolan-bundle.schema.json`. ## Bundle layout ```text -orient/ +/ manifest.json hotspots.jsonl hotspots-full.jsonl # all hotspots before budget truncation @@ -48,9 +48,9 @@ Kinds: `duplication`, `static-finding`, `config`, `dep-hub`, `debt-candidate`. - **FR-001**: No LLM summary as evidence; `producer_ref` required for observed hotspots. - **FR-002**: `gaps.jsonl` budget: top 20 by default. -- **FR-003**: `scripts/orient-export-from-map.sh` maps legacy `portolan map` output to orient layout. +- **FR-003**: `scripts/portolan-export-from-map.sh` maps legacy `portolan map` output to Portolan bundle layout. ## Success Criteria -- **SC-001**: `jq empty harness/contracts/orient-bundle.schema.json` +- **SC-001**: `jq empty harness/contracts/portolan-bundle.schema.json` - **SC-002**: Smoke fixture bundle loads in viewer. diff --git a/docs/specs/088-orient-bundle-contract/tasks.md b/docs/specs/088-orient-bundle-contract/tasks.md index b3cb7b0c..d4050360 100644 --- a/docs/specs/088-orient-bundle-contract/tasks.md +++ b/docs/specs/088-orient-bundle-contract/tasks.md @@ -1,6 +1,6 @@ # Tasks: Orient Bundle Contract -- [x] T001 Publish orient bundle schema. -- [x] T002 Implement build-orient-bundle.sh. -- [x] T003 Implement orient-export-from-map.sh bridge. +- [x] T001 Publish Portolan bundle schema. +- [x] T002 Implement build-portolan-bundle.sh. +- [x] T003 Implement portolan-export-from-map.sh bridge. - [x] T004 Add fixture bundle for viewer smoke. diff --git a/docs/specs/089-orient-wizard/plan.md b/docs/specs/089-orient-wizard/plan.md index a8fc80e1..bfe4efb8 100644 --- a/docs/specs/089-orient-wizard/plan.md +++ b/docs/specs/089-orient-wizard/plan.md @@ -4,15 +4,15 @@ ## Deliverables -- [`scripts/orient-wizard.sh`](../../../scripts/orient-wizard.sh) -- Fixes to [`scripts/build-orient-bundle.sh`](../../../scripts/build-orient-bundle.sh) +- [`scripts/portolan-scan.sh`](../../../scripts/portolan-scan.sh) +- Fixes to [`scripts/build-portolan-bundle.sh`](../../../scripts/build-portolan-bundle.sh) - Smoke evidence in `reviews/` ## Verification ```bash -bash -n scripts/orient-wizard.sh -scripts/orient-wizard.sh --help -scripts/harness-orient-smoke.sh -scripts/orient-wizard.sh . /tmp/orient-portolan --no-viewer --yes +bash -n scripts/portolan-scan.sh +scripts/portolan-scan.sh --help +scripts/harness-portolan-smoke.sh +scripts/portolan-scan.sh . /tmp/portolan-portolan --no-viewer --yes ``` diff --git a/docs/specs/089-orient-wizard/reviews/smoke-findings.md b/docs/specs/089-orient-wizard/reviews/smoke-findings.md index 312536f8..b5a1d417 100644 --- a/docs/specs/089-orient-wizard/reviews/smoke-findings.md +++ b/docs/specs/089-orient-wizard/reviews/smoke-findings.md @@ -5,7 +5,7 @@ ## Smoke A — portolan repo ```bash -scripts/orient-wizard.sh . /tmp/orient-portolan --no-viewer --yes +scripts/portolan-scan.sh . /tmp/portolan-portolan --no-viewer --yes ``` | Metric | Value | @@ -13,18 +13,18 @@ scripts/orient-wizard.sh . /tmp/orient-portolan --no-viewer --yes | Runtime | ~18s (jscpd ~7s, semgrep/syft fast on single repo) | | Hotspots | 128 (0 gaps) | | Top kinds | duplication (jscpd), static-finding (semgrep) | -| Viewer | `node viewer/scripts/serve.js --bundle /tmp/orient-portolan` — page loads, hotspots.jsonl served | +| Viewer | `node viewer/scripts/serve.js --bundle /tmp/portolan-portolan` — page loads, hotspots.jsonl served | ### Fixes applied during smoke 1. **jscpd**: `--noSymlinks true` passed `true` as a path; replaced with `--noSymlinks` + ignore globs. -2. **CLI flags**: options after positionals (`target orient --no-viewer`) were ignored; positional/flag parsing fixed. +2. **CLI flags**: options after positionals (`target bundle-dir --no-viewer`) were ignored; positional/flag parsing fixed. 3. **jscpd output**: read from `producers/jscpd/**/jscpd-report.json` (wizard layout). ## Smoke B — bigtop-landscape (bounded) ```bash -scripts/orient-wizard.sh ~/projects/bigtop-landscape/repos /tmp/orient-bigtop \ +scripts/portolan-scan.sh ~/projects/bigtop-landscape/repos /tmp/portolan-bigtop \ --no-viewer --yes --limit-repos 3 --producers semgrep,syft ``` @@ -49,7 +49,7 @@ scripts/orient-wizard.sh ~/projects/bigtop-landscape/repos /tmp/orient-bigtop \ ## Regression -- `scripts/harness-orient-smoke.sh` — **ok** after bundle ranking/budget changes. +- `scripts/harness-portolan-smoke.sh` — **ok** after bundle ranking/budget changes. ## Skip-install path diff --git a/docs/specs/089-orient-wizard/spec.md b/docs/specs/089-orient-wizard/spec.md index e62ba430..dbfceebd 100644 --- a/docs/specs/089-orient-wizard/spec.md +++ b/docs/specs/089-orient-wizard/spec.md @@ -1,19 +1,19 @@ # Feature Specification: Orient Wizard -**Feature Branch**: `codex/089-orient-wizard` +**Feature Branch**: `codex/089-portolan-scan` **Created**: 2026-06-10 **Status**: Implemented (local verification + smoke evidence in reviews/). -**Input**: One-command orient workflow: check tools, consent install, run recipes, +**Input**: One-command Portolan scan workflow: check tools, consent install, run recipes, build bundle, open viewer. Real-target smoke on portolan and bounded bigtop. ## User Scenarios ### User Story 1 - One Command Orient (Priority: P1) -An operator runs `scripts/orient-wizard.sh ` and receives +An operator runs `scripts/portolan-scan.sh ` and receives a ranked hotspot bundle plus optional local viewer without manual recipe steps. ### User Story 2 - Consent-Gated Tool Install (Priority: P1) @@ -28,7 +28,7 @@ large landscapes without full-root OOM. ## Requirements -- **FR-001**: `orient-wizard.sh` orchestrates tool check, recipes, bundle build, summary. +- **FR-001**: `portolan-scan.sh` orchestrates tool check, recipes, bundle build, summary. - **FR-002**: Install only after explicit y/N or `--yes`; `--skip-install` never installs. - **FR-003**: Shard failures log to `producers/_failures.log` and do not abort the run. - **FR-004**: Flags: `--yes`, `--skip-install`, `--no-viewer`, `--port`, `--limit-repos`, `--producers`. @@ -36,6 +36,6 @@ large landscapes without full-root OOM. ## Success Criteria -- **SC-001**: `orient-wizard.sh . /tmp/orient-portolan --no-viewer` produces hotspots on portolan repo. +- **SC-001**: `portolan-scan.sh . /tmp/portolan-demo --no-viewer` produces hotspots on portolan repo. - **SC-002**: Bounded bigtop smoke completes without crash. -- **SC-003**: `harness-orient-smoke.sh` regression still passes. +- **SC-003**: `harness-portolan-smoke.sh` regression still passes. diff --git a/docs/specs/089-orient-wizard/tasks.md b/docs/specs/089-orient-wizard/tasks.md index 774102ae..ff48a7e6 100644 --- a/docs/specs/089-orient-wizard/tasks.md +++ b/docs/specs/089-orient-wizard/tasks.md @@ -1,7 +1,7 @@ # Tasks: Orient Wizard - [x] T001 Spec 089 artifacts. -- [x] T002 Implement orient-wizard.sh. -- [x] T003 build-orient-bundle.sh ranking and budget fixes. +- [x] T002 Implement portolan-scan.sh. +- [x] T003 build-portolan-bundle.sh ranking and budget fixes. - [x] T004 Smoke A portolan + Smoke B bigtop + reviews/smoke-findings.md. - [x] T005 Docs: SKILL, README, INSTALL-PROMPT, backlog, AGENTS. diff --git a/docs/specs/090-viewer-demo-ux/plan.md b/docs/specs/090-viewer-demo-ux/plan.md index e4886bc0..66cbcac2 100644 --- a/docs/specs/090-viewer-demo-ux/plan.md +++ b/docs/specs/090-viewer-demo-ux/plan.md @@ -7,12 +7,12 @@ - [viewer/src/](../../../viewer/src/) — search, filters, heat tree, detail + source - [viewer/scripts/serve.js](../../../viewer/scripts/serve.js) — `/source` endpoint - [docs/demo-runbook.md](../../../docs/demo-runbook.md) -- Smoke extensions in [scripts/harness-orient-smoke.sh](../../../scripts/harness-orient-smoke.sh) +- Smoke extensions in [scripts/harness-portolan-smoke.sh](../../../scripts/harness-portolan-smoke.sh) ## Verification ```bash node viewer/scripts/build-static.js -scripts/harness-orient-smoke.sh -scripts/orient-wizard.sh . /tmp/orient-portolan --no-viewer --yes +scripts/harness-portolan-smoke.sh +scripts/portolan-scan.sh . /tmp/portolan-portolan --no-viewer --yes ``` diff --git a/docs/specs/090-viewer-demo-ux/reviews/demo-findings.md b/docs/specs/090-viewer-demo-ux/reviews/demo-findings.md index 65e4bc38..81362616 100644 --- a/docs/specs/090-viewer-demo-ux/reviews/demo-findings.md +++ b/docs/specs/090-viewer-demo-ux/reviews/demo-findings.md @@ -4,7 +4,7 @@ ## Smoke regression -`scripts/harness-orient-smoke.sh` — **ok** +`scripts/harness-portolan-smoke.sh` — **ok** New checks: - `id="search-input"` in HTML @@ -13,7 +13,7 @@ New checks: ## Demo A — portolan bundle -Bundle: `/tmp/orient-portolan` (128 hotspots, 0 gaps) +Bundle: `/tmp/portolan-portolan` (128 hotspots, 0 gaps) | Check | Result | | --- | --- | @@ -24,7 +24,7 @@ Bundle: `/tmp/orient-portolan` (128 hotspots, 0 gaps) ## Demo B — bigtop bundle (bounded) -Bundle: `/tmp/orient-bigtop` (200 of 830 hotspots, truncated) +Bundle: `/tmp/portolan-bigtop` (200 of 830 hotspots, truncated) | Check | Result | | --- | --- | diff --git a/docs/specs/090-viewer-demo-ux/spec.md b/docs/specs/090-viewer-demo-ux/spec.md index 944a4159..2227c37b 100644 --- a/docs/specs/090-viewer-demo-ux/spec.md +++ b/docs/specs/090-viewer-demo-ux/spec.md @@ -6,7 +6,7 @@ **Status**: Implemented (smoke + demo evidence in reviews/). -**Input**: Demo-ready orient viewer: search, filters, directory heat tree, source +**Input**: Demo-ready Portolan viewer: search, filters, directory heat tree, source preview, truncation transparency. ## User Scenarios @@ -36,5 +36,5 @@ When hotspot budget truncated the bundle, the viewer shows how many were omitted ## Success Criteria -- **SC-001**: harness-orient-smoke passes with search UI and /source path-safety checks. +- **SC-001**: harness-portolan-smoke passes with search UI and /source path-safety checks. - **SC-002**: Demo works on portolan bundle (~128 hotspots) and bigtop bundle (200). diff --git a/docs/specs/091-landscape-scale/plan.md b/docs/specs/091-landscape-scale/plan.md index 0e3f4416..aba7ef8b 100644 --- a/docs/specs/091-landscape-scale/plan.md +++ b/docs/specs/091-landscape-scale/plan.md @@ -5,7 +5,7 @@ ## Verification ```bash -scripts/harness-orient-smoke.sh -scripts/orient-wizard.sh ~/projects/bigtop-landscape/repos /tmp/orient-bigtop-full \ +scripts/harness-portolan-smoke.sh +scripts/portolan-scan.sh ~/projects/bigtop-landscape/repos /tmp/portolan-bigtop-full \ --no-viewer --yes --shard-timeout 600 ``` diff --git a/docs/specs/091-landscape-scale/reviews/merge-closeout-2026-06-10.md b/docs/specs/091-landscape-scale/reviews/merge-closeout-2026-06-10.md index 05a95d61..0d5b0156 100644 --- a/docs/specs/091-landscape-scale/reviews/merge-closeout-2026-06-10.md +++ b/docs/specs/091-landscape-scale/reviews/merge-closeout-2026-06-10.md @@ -21,8 +21,8 @@ | --- | --- | | `gh pr checks 64` | pass (Baseline, Analyze go/python/actions, CodeQL) | | `go test ./...` | pass | -| `scripts/harness-orient-smoke.sh` | ok | -| User smoke | `scripts/orient-wizard.sh . /tmp/orient-check --no-viewer --yes` → 128 hotspots, 0 gaps | +| `scripts/harness-portolan-smoke.sh` | ok | +| User smoke | `scripts/portolan-scan.sh . /tmp/portolan-check --no-viewer --yes` → 128 hotspots, 0 gaps | ## Review evidence diff --git a/docs/specs/091-landscape-scale/reviews/pr-readiness-closeout-2026-06-10.md b/docs/specs/091-landscape-scale/reviews/pr-readiness-closeout-2026-06-10.md index d883a41c..4dc62a0c 100644 --- a/docs/specs/091-landscape-scale/reviews/pr-readiness-closeout-2026-06-10.md +++ b/docs/specs/091-landscape-scale/reviews/pr-readiness-closeout-2026-06-10.md @@ -9,7 +9,7 @@ | Surface | State | Evidence | | --- | --- | --- | | Local implementation | verified | specs 087–091 tasks complete; review-fix commit applied | -| Local verification | verified | `go test -count=1 ./...`, `go vet ./...`, `jq empty` schemas, `scripts/harness-orient-smoke.sh` | +| Local verification | verified | `go test -count=1 ./...`, `go vet ./...`, `jq empty` schemas, `scripts/harness-portolan-smoke.sh` | | Review evidence | verified | 3 replacement lanes + disposition in `reviews/pr-review-disposition-2026-06-10.md` | | PR state | ready-for-review | marked ready on `e2c7695` after all checks green | | GitHub checks | verified | Baseline + Analyze (go/python/actions) + CodeQL success on `e2c7695` | diff --git a/docs/specs/091-landscape-scale/reviews/pr-review-code-replacement-2026-06-10.md b/docs/specs/091-landscape-scale/reviews/pr-review-code-replacement-2026-06-10.md index 64ded711..04af975b 100644 --- a/docs/specs/091-landscape-scale/reviews/pr-review-code-replacement-2026-06-10.md +++ b/docs/specs/091-landscape-scale/reviews/pr-review-code-replacement-2026-06-10.md @@ -10,13 +10,13 @@ | Severity | File | Fix | | --- | --- | --- | -| P1 | `build-orient-bundle.sh:137` | dep-hub sentinel path | -| P1 | `orient-wizard.sh:45` | `require_opt_value` for value flags | -| P2 | `orient-wizard.sh:90` | shard gap status `cannot_verify` | -| P2 | `build-orient-bundle.sh:173` | integer validation for budgets | +| P1 | `build-portolan-bundle.sh:137` | dep-hub sentinel path | +| P1 | `portolan-scan.sh:45` | `require_opt_value` for value flags | +| P2 | `portolan-scan.sh:90` | shard gap status `cannot_verify` | +| P2 | `build-portolan-bundle.sh:173` | integer validation for budgets | | P2 | `viewer/src/app.js:104` | dep-hub bypasses repo filter | -| P2 | `build-orient-bundle.sh:222` | gap budget cap | -| P3 | `orient-wizard.sh:404` | node check only when viewer starts | +| P2 | `build-portolan-bundle.sh:222` | gap budget cap | +| P3 | `portolan-scan.sh:404` | node check only when viewer starts | ## Deferred (not blocking MVP) diff --git a/docs/specs/091-landscape-scale/reviews/pr-review-disposition-2026-06-10.md b/docs/specs/091-landscape-scale/reviews/pr-review-disposition-2026-06-10.md index 2ce1a4e0..41b8a242 100644 --- a/docs/specs/091-landscape-scale/reviews/pr-review-disposition-2026-06-10.md +++ b/docs/specs/091-landscape-scale/reviews/pr-review-disposition-2026-06-10.md @@ -40,7 +40,7 @@ ## Verification (post-fix) ```text -scripts/harness-orient-smoke.sh — ok +scripts/harness-portolan-smoke.sh — ok go test ./... — ok ``` diff --git a/docs/specs/091-landscape-scale/reviews/pr-review-requirements-replacement-2026-06-10.md b/docs/specs/091-landscape-scale/reviews/pr-review-requirements-replacement-2026-06-10.md index 3593ac98..72cf0219 100644 --- a/docs/specs/091-landscape-scale/reviews/pr-review-requirements-replacement-2026-06-10.md +++ b/docs/specs/091-landscape-scale/reviews/pr-review-requirements-replacement-2026-06-10.md @@ -10,10 +10,10 @@ | ID | Severity | Fix | | --- | --- | --- | -| REQ-001 | major | `build-orient-bundle.sh`: `ORIENT_GAP_BUDGET` default 20, sort + cap gaps | +| REQ-001 | major | `build-portolan-bundle.sh`: `PORTOLAN_GAP_BUDGET` default 20, sort + cap gaps | | REQ-002 | major | dep-hub `paths: ["(dependency-hub)"]` | | REQ-003 | major | shard gaps map to `cannot_verify` (schema enum) | -| REQ-004 | major | `harness-orient-smoke.sh` validates hotspot/gap records | +| REQ-004 | major | `harness-portolan-smoke.sh` validates hotspot/gap records | | REQ-007 | minor | semgrep: no `p/default` fallback; gap when local rules missing | ## Rejected / deferred diff --git a/docs/specs/091-landscape-scale/reviews/pr-review-security-replacement-2026-06-10.md b/docs/specs/091-landscape-scale/reviews/pr-review-security-replacement-2026-06-10.md index e80d2d4b..e348489c 100644 --- a/docs/specs/091-landscape-scale/reviews/pr-review-security-replacement-2026-06-10.md +++ b/docs/specs/091-landscape-scale/reviews/pr-review-security-replacement-2026-06-10.md @@ -21,4 +21,4 @@ ## Test added -- `harness-orient-smoke.sh`: symlink `leak-outside` → `/etc/passwd` returns 403 +- `harness-portolan-smoke.sh`: symlink `leak-outside` → `/etc/passwd` returns 403 diff --git a/docs/specs/091-landscape-scale/reviews/scale-findings.md b/docs/specs/091-landscape-scale/reviews/scale-findings.md index b1730baa..716b8a00 100644 --- a/docs/specs/091-landscape-scale/reviews/scale-findings.md +++ b/docs/specs/091-landscape-scale/reviews/scale-findings.md @@ -5,7 +5,7 @@ ## Smoke C command ```bash -scripts/orient-wizard.sh ~/projects/bigtop-landscape/repos /tmp/orient-bigtop-full \ +scripts/portolan-scan.sh ~/projects/bigtop-landscape/repos /tmp/portolan-bigtop-full \ --no-viewer --yes --shard-timeout 600 --jscpd-memory-mb 2048 ``` diff --git a/docs/specs/091-landscape-scale/spec.md b/docs/specs/091-landscape-scale/spec.md index 8203f1d7..24531c87 100644 --- a/docs/specs/091-landscape-scale/spec.md +++ b/docs/specs/091-landscape-scale/spec.md @@ -6,14 +6,14 @@ **Status**: Implemented (scale evidence in reviews/scale-findings.md). -**Input**: Full bigtop landscape (18 repos) through orient-wizard without OOM abort; +**Input**: Full bigtop landscape (18 repos) through portolan-scan without OOM abort; shard failures as honest gaps; scalable bundle build with kind-aware budget. ## Requirements -- **FR-001**: `--jscpd-memory-mb` and `--shard-timeout` on orient-wizard.sh. +- **FR-001**: `--jscpd-memory-mb` and `--shard-timeout` on portolan-scan.sh. - **FR-002**: Per-shard failures written to `producers/_gaps.jsonl`. -- **FR-003**: Single-pass jq per producer file in build-orient-bundle.sh. +- **FR-003**: Single-pass jq per producer file in build-portolan-bundle.sh. - **FR-004**: Kind-quota hotspot budget; `hotspots-full.jsonl` for agents. - **FR-005**: Smoke C on full bigtop with evidence in reviews/. diff --git a/docs/specs/092-orient-surfaces/plan.md b/docs/specs/092-orient-surfaces/plan.md index efd5aebd..01472d16 100644 --- a/docs/specs/092-orient-surfaces/plan.md +++ b/docs/specs/092-orient-surfaces/plan.md @@ -2,9 +2,9 @@ ## Slice 0 — PR #64 debt -- `repo_slug` hash in orient-wizard.sh +- `repo_slug` hash in portolan-scan.sh - spec 088 layout: hotspots-full.jsonl, manifest fields -- harness-orient-smoke: filter-bar, heat-tree, status-banner, truncation test +- harness-portolan-smoke: filter-bar, heat-tree, status-banner, truncation test - CI: wizard --skip-install on fixture target ## Slice 1 — config-surfaces @@ -12,7 +12,7 @@ - `scripts/scan-config-surfaces.sh` - `harness/recipes/config-surfaces.md` - wizard `run_config` + bundle jq + kind quotas -- fixture config files under orient-bundle/target +- fixture config files under portolan-bundle/target ## Slice 2 — ctags @@ -26,7 +26,7 @@ ## Slice 3.5 — gitignore hardening -- `scripts/orient-ignore.sh` + `git check-ignore` fallback patterns +- `scripts/portolan-ignore.sh` + `git check-ignore` fallback patterns - jscpd `--gitignore`, ctags `git ls-files -co --exclude-standard`, config scan filter -- bundle post-filter in `build-orient-bundle.sh` -- harness-orient-smoke: assert ignored paths excluded from fixture bundle +- bundle post-filter in `build-portolan-bundle.sh` +- harness-portolan-smoke: assert ignored paths excluded from fixture bundle diff --git a/docs/specs/092-orient-surfaces/reviews/merge-closeout-2026-06-10.md b/docs/specs/092-orient-surfaces/reviews/merge-closeout-2026-06-10.md index 18e51f1d..a570a281 100644 --- a/docs/specs/092-orient-surfaces/reviews/merge-closeout-2026-06-10.md +++ b/docs/specs/092-orient-surfaces/reviews/merge-closeout-2026-06-10.md @@ -20,7 +20,7 @@ - Config surfaces producer + bundle `kind=config` - ctags symbol density + `kind=debt-candidate` - Kind-quota budget + PR #64 debt closure -- FR-007 gitignore hardening (`orient-ignore.sh`) +- FR-007 gitignore hardening (`portolan-ignore.sh`) - Real-target smoke in `reviews/smoke-findings.md` ## Stop reason diff --git a/docs/specs/092-orient-surfaces/reviews/pr-readiness-closeout-2026-06-10.md b/docs/specs/092-orient-surfaces/reviews/pr-readiness-closeout-2026-06-10.md index 22bd6966..4c7327ce 100644 --- a/docs/specs/092-orient-surfaces/reviews/pr-readiness-closeout-2026-06-10.md +++ b/docs/specs/092-orient-surfaces/reviews/pr-readiness-closeout-2026-06-10.md @@ -9,7 +9,7 @@ | Surface | State | Evidence | | --- | --- | --- | | Local implementation | verified | tasks.md complete; slices 0–3 + 3.5 (gitignore) | -| Local verification | verified | go test/vet, jq schema, harness-orient-smoke, git diff --check | +| Local verification | verified | go test/vet, jq schema, harness-portolan-smoke, git diff --check | | Review evidence | verified | 4 assessed lanes + local on review-fix head; post-gitignore harness-only diff — repo-grounded re-check only | | PR state | ready-for-review | `gh pr view 65` isDraft=false | | GitHub checks | pending re-verify | force-push harness-only head; expect Baseline + CodeQL green | @@ -22,7 +22,7 @@ - ctags symbol density + `kind=debt-candidate` - PR #64 debt closure (repo_slug, smoke, CI wizard) - Review-fix: gaps, multi-repo paths, stronger smoke/CI -- Slice 3.5: `.gitignore` across producers + bundle post-filter (`orient-ignore.sh`) +- Slice 3.5: `.gitignore` across producers + bundle post-filter (`portolan-ignore.sh`) ## Out of scope (follow-up) diff --git a/docs/specs/092-orient-surfaces/reviews/pr-review-disposition-2026-06-10.md b/docs/specs/092-orient-surfaces/reviews/pr-review-disposition-2026-06-10.md index aa766763..d4b7cca1 100644 --- a/docs/specs/092-orient-surfaces/reviews/pr-review-disposition-2026-06-10.md +++ b/docs/specs/092-orient-surfaces/reviews/pr-review-disposition-2026-06-10.md @@ -36,9 +36,9 @@ CORR-003..005, SEC-092-003..006, TEST-001/004/005/008/009 (documented), pattern ```bash go test ./... go vet ./... -jq empty harness/contracts/orient-bundle.schema.json -scripts/harness-orient-smoke.sh -scripts/orient-wizard.sh internal/testfixtures/orient-bundle/target /tmp/wizard-ci --no-viewer --skip-install --yes +jq empty harness/contracts/portolan-bundle.schema.json +scripts/harness-portolan-smoke.sh +scripts/portolan-scan.sh internal/testfixtures/portolan-bundle/target /tmp/wizard-ci --no-viewer --skip-install --yes ``` ## Readiness diff --git a/docs/specs/092-orient-surfaces/reviews/smoke-findings.md b/docs/specs/092-orient-surfaces/reviews/smoke-findings.md index 508d33b1..4f712e97 100644 --- a/docs/specs/092-orient-surfaces/reviews/smoke-findings.md +++ b/docs/specs/092-orient-surfaces/reviews/smoke-findings.md @@ -6,7 +6,7 @@ ## Smoke A — portolan (self) ```bash -scripts/orient-wizard.sh . /tmp/orient-smoke-a --no-viewer --skip-install --yes +scripts/portolan-scan.sh . /tmp/portolan-smoke-a --no-viewer --skip-install --yes ``` | Metric | Value | @@ -28,7 +28,7 @@ Re-run on harness-only head: ignored agent artifact paths no longer appear in bu ## Smoke B — bigtop bounded (3 repos) ```bash -scripts/orient-wizard.sh ~/projects/bigtop-landscape/repos /tmp/orient-smoke-b \ +scripts/portolan-scan.sh ~/projects/bigtop-landscape/repos /tmp/portolan-smoke-b \ --no-viewer --skip-install --yes --limit-repos 3 ``` @@ -44,12 +44,12 @@ config + ctags producers ran per repo; bundle kind mix includes duplication, con ## Fixture smoke (CI) -`scripts/harness-orient-smoke.sh` — **verified** locally: +`scripts/harness-portolan-smoke.sh` — **verified** locally: - kind `config` and `debt-candidate` in fixture bundle - viewer DOM: `filter-bar`, `heat-tree`, `status-banner` -- `ORIENT_HOTSPOT_BUDGET=2` truncation manifest + full list length +- `PORTOLAN_HOTSPOT_BUDGET=2` truncation manifest + full list length ## Wizard CI orchestration -`orient-wizard.sh` on fixture with `--skip-install --no-viewer` — gaps non-empty (`not_assessed` for missing syft when tools skipped). +`portolan-scan.sh` on fixture with `--skip-install --no-viewer` — gaps non-empty (`not_assessed` for missing syft when tools skipped). diff --git a/docs/specs/092-orient-surfaces/spec.md b/docs/specs/092-orient-surfaces/spec.md index 90f90979..02cbb87a 100644 --- a/docs/specs/092-orient-surfaces/spec.md +++ b/docs/specs/092-orient-surfaces/spec.md @@ -6,7 +6,7 @@ **Status**: Merged via PR #65 (`8025de3`) -**Input**: Add config-surfaces inventory and ctags symbol-density layers to the orient harness path; close PR #64 deferred debt items. +**Input**: Add config-surfaces inventory and ctags symbol-density layers to the Portolan harness path; close PR #64 deferred debt items. **Out of scope**: Viewer UX polish (views, layout, product naming) — follow-up PR `codex/viewer-ux-followup` / spec 093. @@ -14,7 +14,7 @@ ### User Story 1 - Config surface inventory (Priority: P1) -An agent runs orient-wizard and sees `config` hotspots grouped by surface kind (Dockerfile, compose, k8s, env, CI, terraform) without installing external tools. +An agent runs portolan-scan and sees `config` hotspots grouped by surface kind (Dockerfile, compose, k8s, env, CI, terraform) without installing external tools. ### User Story 2 - Symbol-dense files (Priority: P1) @@ -31,8 +31,8 @@ repo_slug hash, spec 088 layout docs, extended harness smoke, wizard orchestrati - **FR-003**: Bundle maps config → kind `config`, ctags density → kind `debt-candidate`. - **FR-004**: Kind-quota budget: static-finding 45%, duplication 25%, dep-hub 15%, config 15%, remainder debt-candidate. - **FR-005**: repo_slug includes path hash suffix to avoid basename collisions. -- **FR-006**: harness-orient-smoke checks viewer DOM markers and truncation; CI runs wizard with `--skip-install`. -- **FR-007**: Producers and bundle post-filter respect `.gitignore` (`scripts/orient-ignore.sh`, jscpd `--gitignore`, ctags via `git ls-files`, config scan via `git check-ignore`). +- **FR-006**: harness-portolan-smoke checks viewer DOM markers and truncation; CI runs wizard with `--skip-install`. +- **FR-007**: Producers and bundle post-filter respect `.gitignore` (`scripts/portolan-ignore.sh`, jscpd `--gitignore`, ctags via `git ls-files`, config scan via `git check-ignore`). ## Success Criteria diff --git a/docs/specs/093-viewer-landscape-ux/plan.md b/docs/specs/093-viewer-landscape-ux/plan.md new file mode 100644 index 00000000..1aad31e1 --- /dev/null +++ b/docs/specs/093-viewer-landscape-ux/plan.md @@ -0,0 +1,29 @@ +# Implementation Plan: Landscape Report Viewer (093) + +## Slice 1–2 (done) — interim UX + +Portolan naming, explainers, folder tree + list (superseded by tabbed report). + +## Slice 3b — CLI naming + +- `portolan-scan.sh`, `build-portolan-bundle.sh`, `harness-portolan-smoke.sh`, `portolan-ignore.sh` +- Remove misnamed legacy scripts (no deprecation wrappers) + +## Slice 4 — Report artifacts + +- `scripts/scan-landscape-card.sh` → `landscape-card.json` +- `build-portolan-bundle.sh` → `landscape-report.json` + enriched `graph-slice.json` +- `harness/contracts/landscape-card.schema.json`, `landscape-report.schema.json` + +## Slice 5 — Report viewer shell + +- Tabs: Overview | Findings | Gaps (default Overview) +- Overview renders card + repo matrix + next steps + +## Slice 6 — Findings tab + +- Sections by kind, folder map, inspector, load-all findings, source preview + +## Slice 7 — Ship + +- Smoke, demo evidence, PR #66 ready-for-review diff --git a/docs/specs/093-viewer-landscape-ux/reviews/demo-evidence-2026-06-10.md b/docs/specs/093-viewer-landscape-ux/reviews/demo-evidence-2026-06-10.md new file mode 100644 index 00000000..83d4792b --- /dev/null +++ b/docs/specs/093-viewer-landscape-ux/reviews/demo-evidence-2026-06-10.md @@ -0,0 +1,37 @@ +# Demo evidence — spec 093 (2026-06-10) + +## Verification + +| Check | Result | +| --- | --- | +| `scripts/harness-portolan-smoke.sh` | **ok** | +| `go test ./...` | **ok** | +| `jq empty harness/contracts/landscape-*.schema.json` | **ok** | +| Viewer default tab | Overview (`#report-overview`) | +| Bundle artifacts | `landscape-card.json`, `landscape-report.json`, `graph-slice.json` | + +## Fixture smoke bundle (`portolan-smoke`) + +- **Identity**: fixture target name from `landscape-card.json` +- **Findings**: 7 shown / 7 total (no truncation in fixture) +- **Gaps**: 1 (`dependencies` not_assessed when syft skipped) +- **Tabs**: Overview loads card + next steps; Findings has sections + tree; Gaps lists gap row + +## Demo bar (operator questions) + +| Question | Evidence | +| --- | --- | +| What is this target? | Overview → language, files, LOC, maturity badges | +| How many repos? | Overview → repo table (1 in fixture) | +| Top problems? | Overview → “Where to look first” (ranked from report) | +| What was not checked? | Gaps tab → syft/dependencies `not_assessed` | + +## Comparison notes + +- **landscape-card.json**: standalone Portolan contract (identity, scale, maturity, health_signals); no external scout runtime. +- **map.md**: `landscape-report.json` sections `repos`, `findings_by_kind`, `gaps`, `next_steps` align with `writeMap` section intent; full Go `portolan map` bridge deferred to spec 094. + +## Not assessed + +- Live bigtop re-scan for this slice (use `docs/demo-runbook.md` full landscape command). +- Independent PR review lanes (pending PR readiness closeout). diff --git a/docs/specs/093-viewer-landscape-ux/reviews/pr-review-disposition-2026-06-11.md b/docs/specs/093-viewer-landscape-ux/reviews/pr-review-disposition-2026-06-11.md new file mode 100644 index 00000000..b61f9184 --- /dev/null +++ b/docs/specs/093-viewer-landscape-ux/reviews/pr-review-disposition-2026-06-11.md @@ -0,0 +1,63 @@ +# PR #66 review disposition — 2026-06-11 + +**Head**: `abcf87b` (fix viewer load-all sync + attribute escaping) +**PR**: https://github.com/fcon-tech/portolan/pull/66 + +## Verification + +| Check | Status | +| --- | --- | +| `scripts/harness-portolan-smoke.sh` | verified | +| `go test ./...` | verified (prior runs) | +| GitHub Baseline + CodeQL | verified (SUCCESS on `abcf87b` push) | +| OpenCode independent lanes (glm/kimi/minimax) | not_assessed — prior runs hung, no `result.md` | + +## Review lanes (iteration 3 — post `abcf87b`) + +| Lane | Model / harness | Status | Verdict | +| --- | --- | --- | --- | +| Repo-grounded | cavecrew-reviewer | assessed | LGTM (0 blockers, 2 residual 🟡) | +| Correctness | ce-correctness-reviewer | assessed | LGTM | +| Security | ce-security-reviewer | assessed | LGTM | +| Requirements | zai-coding-plan/glm-5.1 | not_assessed | — | +| Code | kimi-for-coding/k2p6 | not_assessed | — | +| Security (OpenCode) | minimax/MiniMax-M2.7 | not_assessed | — | + +## Fixed (was blocking) + +| ID | Severity | Finding | Fix commit | +| --- | --- | --- | --- | +| R1 | major | Language display `[object Object]` | `517fd47` | +| R2 | major | Gaps `recipe_ref` vs `recipe` | `517fd47` | +| R3 | major | Findings sections stale after load-all | `517fd47` | +| R4 | major | Truncation banner after load-all | `abcf87b` | +| R5 | major | Overview scale/kind counts stale after load-all | `abcf87b` | +| R6 | major | `escapeHtml` in attributes → attribute-breakout XSS | `abcf87b` | +| R7 | major | Filter chip counts stale after load-all | `abcf87b` | +| R8 | minor | Findings sections vs active filters misaligned | `abcf87b` | +| R9 | minor | Kind section order not map.md order | `abcf87b` | +| R10 | minor | Manifest footer stale after load-all | `abcf87b` | + +## Accepted residuals (non-blocking) + +| ID | Finding | Disposition | +| --- | --- | --- | +| Y1 | Empty `hotspots-full.jsonl` → silent load-all click | accepted — edge case; bundle contract expects file when truncated | +| Y2 | Short full file vs `manifest.hotspots_total` mismatch | accepted — pre-existing bundle contract assumption | +| Y3 | Manifest numeric fields in banner innerHTML unescaped | accepted — local bundle threat model; low risk | +| Y4 | Overview `renderOverview()` on load-all only when on overview tab | accepted — tab switch re-renders via `switchTab` | + +## Rejected / deferred + +| ID | Finding | Disposition | +| --- | --- | --- | +| D1 | Overview does not render all `landscape-report.json` blocks | deferred — slice 5 delivered card + next_steps | +| D2 | `total_loc` always unknown | deferred — scanner follow-up | +| D3 | Loose landscape schemas + no CI artifact validation | deferred | +| D4 | Silent stub card on scan-landscape-card failure | accepted as minor | + +## Merge readiness + +**Local implementation: LGTM** — all assessed local review lanes pass; no blocking findings remain. + +**PR #66: ready-for-review** — CI green on `abcf87b`. OpenCode independent lanes (glm/kimi/minimax) remain `not_assessed`; merge per AGENTS.md still needs explicit user approval and optional OpenCode lane retry. diff --git a/docs/specs/093-viewer-landscape-ux/spec.md b/docs/specs/093-viewer-landscape-ux/spec.md new file mode 100644 index 00000000..51af92f6 --- /dev/null +++ b/docs/specs/093-viewer-landscape-ux/spec.md @@ -0,0 +1,54 @@ +# Feature Specification: Landscape Report Viewer (093) + +**Feature Branch**: `codex/093-viewer-landscape-ux` + +**Created**: 2026-06-10 + +**Status**: Active implementation + +**Input**: Human-readable landscape report in the viewer: Portolan `landscape-card.json` / +`landscape-report.json` and `map.md`-style sections. Resurrects pruned `052-agent-scan-report-ux` +scope. Scanner findings drill-down (UA navigation pattern), not LLM architecture graph. + +**Out of scope**: sdp_lab or any external scout runtime, LLM Q&A, full `portolan map` graph import (spec 094). + +## User Scenarios + +### User Story 1 - Report in 10 seconds (Priority: P1) + +A human opens the viewer and sees a project card (language, scale, maturity) and repo +matrix before diving into individual findings. + +### User Story 2 - Findings by section (Priority: P1) + +Findings appear grouped like `map.md` (duplication, config, smells, symbol density, deps), +not only a flat ranked list. + +### User Story 3 - Gaps visible (Priority: P1) + +A dedicated tab lists what the scan did not assess. + +### User Story 4 - Findings map (Priority: P2) + +Inside the Findings tab, a folder map and inspector support drill-down to source. + +### User Story 5 - Drill-down (Priority: P1) + +Click a finding → detail panel → read-only source preview. + +## Requirements + +- **FR-001**: Product UI and CLI use **Portolan** naming only. +- **FR-002**: Default tab = **Overview** (`landscape-card.json` + `landscape-report.json`). +- **FR-003**: Tabs: Overview | Findings | Gaps. +- **FR-004**: Bundle emits `landscape-card.json` and `landscape-report.json`. +- **FR-005**: Findings grouped by kind/section (map.md parity). +- **FR-006**: When scan found more than viewer shows, UI states «найдено Y, показано X» and offers load-all. +- **FR-007**: Entry point is `portolan-scan.sh` (no legacy misnamed scripts). +- **FR-008**: Report sections use `evidence_ref`; unknown/gap states stay visible. +- **FR-009**: harness-portolan-smoke checks report DOM markers. + +## Success Criteria + +- **SC-001**: harness-portolan-smoke passes with fixture bundle including report artifacts. +- **SC-002**: Operator answers target identity, repo count, top issues, and gaps without external docs. diff --git a/docs/specs/093-viewer-landscape-ux/tasks.md b/docs/specs/093-viewer-landscape-ux/tasks.md new file mode 100644 index 00000000..c0bef964 --- /dev/null +++ b/docs/specs/093-viewer-landscape-ux/tasks.md @@ -0,0 +1,9 @@ +# Tasks: Landscape Report Viewer (093) + +- [x] Spec/plan/tasks (report-first revision) +- [x] Slice 1–2 interim: Portolan naming, list/tree layout +- [x] Slice 3b: portolan-* CLI rename (legacy orient-* scripts removed) +- [x] Slice 4: landscape-card + landscape-report in bundle +- [x] Slice 5: tabbed viewer — Overview default +- [x] Slice 6: Findings tab — sections, map, load-all +- [x] Slice 7: smoke, demo evidence (PR ship — see PR #66) diff --git a/harness/SKILL.md b/harness/SKILL.md index 3ba32f25..88552322 100644 --- a/harness/SKILL.md +++ b/harness/SKILL.md @@ -1,24 +1,24 @@ -# Portolan Orient Harness +# Portolan Harness Use this skill when the user wants to find code pain (duplication, static smells, -dependency hubs) and navigate a local landscape with an orient map. +dependency hubs) and browse ranked hotspots in the local viewer. -Portolan is a **harness supplement**: recipes + guardrails + orient bundle + local +Portolan is a **harness supplement**: recipes + guardrails + evidence bundle + local viewer. The legacy Go CLI is optional (see `docs/harness/GO-FREEZE-POLICY.md`). ## Inputs - `TARGET_PATH` — absolute path to repo or landscape root (read-only). -- `ORIENT_PATH` — absolute empty output directory for the orient bundle. +- `BUNDLE_DIR` — absolute empty output directory for the Portolan bundle (convention: any empty dir). - `PORTOLAN_PATH` — absolute path to this Portolan checkout. -## Workflow (recommended: orient wizard) +## Workflow (recommended: portolan scan) One command checks tools, runs producers (with consent-gated install), builds the bundle, prints a summary, and optionally opens the local viewer: ```bash -"$PORTOLAN_PATH/scripts/orient-wizard.sh" "$TARGET_PATH" "$ORIENT_PATH" --yes +"$PORTOLAN_PATH/scripts/portolan-scan.sh" "$TARGET_PATH" "$BUNDLE_DIR" --yes ``` Useful flags: @@ -50,29 +50,30 @@ When you need fine-grained control, run individual recipes from `harness/recipes | Config / deploy surfaces? | `config-surfaces.md` | | Symbol-dense files (optional) | `symbols-ctags.md` | -Write outputs under `$ORIENT_PATH/producers/`, then: +Write outputs under `$BUNDLE_DIR/producers/`, then: ```bash -"$PORTOLAN_PATH/scripts/build-orient-bundle.sh" "$TARGET_PATH" "$ORIENT_PATH" +"$PORTOLAN_PATH/scripts/build-portolan-bundle.sh" "$TARGET_PATH" "$BUNDLE_DIR" ``` -### Open orient map (human) +### Open viewer (human) ```bash -cd "$PORTOLAN_PATH/viewer" && npm install && npm run serve -- --bundle "$ORIENT_PATH" +cd "$PORTOLAN_PATH/viewer" && npm install && npm run serve -- --bundle "$BUNDLE_DIR" ``` -Viewer features (spec 090): search, kind/severity/repo filters, directory heat map, -truncation/gaps banners, and read-only source preview via `/source` (path-guarded). +Viewer features (spec 090): folder tree, ranked list, search, kind/severity/repo +filters, truncation/gaps banners, and read-only source preview via `/source` +(path-guarded). Demo script: [`docs/demo-runbook.md`](../docs/demo-runbook.md). ### Agent navigation Read in order: -1. `$ORIENT_PATH/manifest.json` -2. `$ORIENT_PATH/hotspots.jsonl` (ranked pain points) -3. `$ORIENT_PATH/gaps.jsonl` (missing evidence — do not invent) +1. `$BUNDLE_DIR/manifest.json` +2. `$BUNDLE_DIR/hotspots.jsonl` (ranked pain points) +3. `$BUNDLE_DIR/gaps.jsonl` (missing evidence — do not invent) Cite `hotspot.id` and `producer_ref` for every material claim. @@ -81,8 +82,8 @@ Guardrails: `harness/guardrails/`. ## Legacy bridge (optional) ```bash -go run "$PORTOLAN_PATH/cmd/portolan" map --root "$TARGET_PATH" --out "$ORIENT_PATH/map" --force -"$PORTOLAN_PATH/scripts/orient-export-from-map.sh" "$ORIENT_PATH/map" "$ORIENT_PATH" +go run "$PORTOLAN_PATH/cmd/portolan" map --root "$TARGET_PATH" --out "$BUNDLE_DIR/map" --force +"$PORTOLAN_PATH/scripts/portolan-export-from-map.sh" "$BUNDLE_DIR/map" "$BUNDLE_DIR" ``` ## User question routing diff --git a/harness/codex-claude/INSTALL-PROMPT.md b/harness/codex-claude/INSTALL-PROMPT.md index c3309baa..edf21ffa 100644 --- a/harness/codex-claude/INSTALL-PROMPT.md +++ b/harness/codex-claude/INSTALL-PROMPT.md @@ -5,18 +5,18 @@ Portable block — same skill as Cursor and OpenCode. ```text PORTOLAN_PATH= TARGET_PATH= -ORIENT_PATH= +BUNDLE_DIR= ``` ```text -Execute Portolan orient harness now (no confirmation unless paths missing). +Execute Portolan harness now (no confirmation unless paths missing). 1. Read PORTOLAN_PATH/harness/SKILL.md -2. PORTOLAN_PATH/scripts/orient-wizard.sh TARGET_PATH ORIENT_PATH --no-viewer --yes +2. PORTOLAN_PATH/scripts/portolan-scan.sh TARGET_PATH BUNDLE_DIR --no-viewer --yes 3. Summarize hotspots.jsonl (top 5 by rank) and gaps.jsonl 4. Apply PORTOLAN_PATH/harness/guardrails/citation-rules.md -Optional viewer for human: cd PORTOLAN_PATH/viewer && npm install && npm run serve -- --bundle ORIENT_PATH +Optional viewer for human: cd PORTOLAN_PATH/viewer && npm install && npm run serve -- --bundle BUNDLE_DIR Legacy Go path only if asked: docs/harness/GO-FREEZE-POLICY.md ``` diff --git a/harness/contracts/landscape-card.schema.json b/harness/contracts/landscape-card.schema.json new file mode 100644 index 00000000..c9fe74f8 --- /dev/null +++ b/harness/contracts/landscape-card.schema.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://portolan.local/schemas/landscape-card.schema.json", + "title": "Portolan Landscape Card", + "type": "object", + "required": ["version", "identity", "scale", "maturity", "health_signals"], + "properties": { + "version": { "type": "string" }, + "scanned_at": { "type": "string" }, + "identity": { "type": "object" }, + "scale": { "type": "object" }, + "activity": { "type": "object" }, + "maturity": { "type": "object" }, + "health_signals": { "type": "object" } + }, + "additionalProperties": true +} diff --git a/harness/contracts/landscape-report.schema.json b/harness/contracts/landscape-report.schema.json new file mode 100644 index 00000000..224b41ea --- /dev/null +++ b/harness/contracts/landscape-report.schema.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://portolan.local/schemas/landscape-report.schema.json", + "title": "Portolan Landscape Report", + "type": "object", + "required": ["schema_version", "sections"], + "properties": { + "schema_version": { "type": "string" }, + "generated_at": { "type": "string" }, + "target_root": { "type": "string" }, + "sections": { + "type": "array", + "items": { + "type": "object", + "required": ["id", "title"], + "properties": { + "id": { "type": "string" }, + "title": { "type": "string" }, + "blocks": { "type": "array" }, + "items": { "type": "array" }, + "groups": { "type": "array" } + } + } + } + }, + "additionalProperties": true +} diff --git a/harness/contracts/orient-bundle.schema.json b/harness/contracts/portolan-bundle.schema.json similarity index 95% rename from harness/contracts/orient-bundle.schema.json rename to harness/contracts/portolan-bundle.schema.json index 4c6ef65c..f8ebe727 100644 --- a/harness/contracts/orient-bundle.schema.json +++ b/harness/contracts/portolan-bundle.schema.json @@ -1,7 +1,7 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://portolan.local/schemas/orient-bundle.schema.json", - "title": "Portolan Orient Bundle", + "$id": "https://portolan.local/schemas/portolan-bundle.schema.json", + "title": "Portolan Bundle", "type": "object", "required": ["manifest", "hotspot_record", "gap_record"], "properties": { diff --git a/harness/cursor/portolan-orient.mdc b/harness/cursor/portolan-harness.mdc similarity index 60% rename from harness/cursor/portolan-orient.mdc rename to harness/cursor/portolan-harness.mdc index 2f302758..378aba87 100644 --- a/harness/cursor/portolan-orient.mdc +++ b/harness/cursor/portolan-harness.mdc @@ -1,17 +1,17 @@ --- -description: Portolan orient harness — recipes, orient bundle, local map viewer +description: Portolan harness — recipes, Portolan bundle, local map viewer globs: alwaysApply: false --- -# Portolan Orient +# Portolan harness When mapping a local codebase for duplication, static findings, or dependency hubs: 1. Read `harness/SKILL.md` in the Portolan checkout. 2. Run approved recipes from `harness/recipes/`. -3. Build bundle: `scripts/build-orient-bundle.sh `. -4. Open viewer: `cd viewer && npm run serve -- --bundle `. +3. Build bundle: `scripts/build-portolan-bundle.sh `. +4. Open viewer: `cd viewer && npm run serve -- --bundle `. 5. Follow `harness/guardrails/citation-rules.md` for answers. Do not require `go install` unless the user asks for the legacy Go path. diff --git a/harness/opencode/INSTALL-PROMPT.md b/harness/opencode/INSTALL-PROMPT.md index 8f55f2ee..298e906f 100644 --- a/harness/opencode/INSTALL-PROMPT.md +++ b/harness/opencode/INSTALL-PROMPT.md @@ -5,21 +5,21 @@ Replace variables with absolute paths, then send the block to the agent. ```text PORTOLAN_PATH= TARGET_PATH= -ORIENT_PATH= +BUNDLE_DIR= ``` ```text -Run the Portolan orient harness on TARGET_PATH. Write the orient bundle to -ORIENT_PATH. Follow PORTOLAN_PATH/harness/SKILL.md. +Run the Portolan harness on TARGET_PATH. Write the Portolan bundle to +BUNDLE_DIR. Follow PORTOLAN_PATH/harness/SKILL.md. Rules: - Use only local paths; no network unless I approve a specific recipe. -- Prefer ORIENT_PATH under PORTOLAN_PATH/.portolan/runs/ if your harness blocks external writes. -- Primary: PORTOLAN_PATH/scripts/orient-wizard.sh TARGET_PATH ORIENT_PATH --no-viewer --yes -- Manual fallback: recipes from PORTOLAN_PATH/harness/recipes/ + build-orient-bundle.sh +- Prefer BUNDLE_DIR under PORTOLAN_PATH/.portolan/runs/ if your harness blocks external writes. +- Primary: PORTOLAN_PATH/scripts/portolan-scan.sh TARGET_PATH BUNDLE_DIR --no-viewer --yes +- Manual fallback: recipes from PORTOLAN_PATH/harness/recipes/ + build-portolan-bundle.sh - Cite hotspot.id and producer_ref per harness/guardrails/citation-rules.md. - Do not invent Portolan commands. Report: hotspot count, top 5 hotspots by rank, gaps, and viewer command: -cd PORTOLAN_PATH/viewer && npm run serve -- --bundle ORIENT_PATH +cd PORTOLAN_PATH/viewer && npm run serve -- --bundle BUNDLE_DIR ``` diff --git a/harness/recipes/config-surfaces.md b/harness/recipes/config-surfaces.md index 9e7826d4..dfb931a2 100644 --- a/harness/recipes/config-surfaces.md +++ b/harness/recipes/config-surfaces.md @@ -7,12 +7,12 @@ ## Bounded scan -Per repository shard (orient-wizard default): +Per repository shard (portolan-scan default): ```bash REPO= SLUG= -OUT=/producers/config/${SLUG}.jsonl +OUT=/producers/config/${SLUG}.jsonl scripts/scan-config-surfaces.sh "$REPO" "$OUT" ``` @@ -23,7 +23,7 @@ scripts/scan-config-surfaces.sh "$REPO" "$OUT" ## Re-ingest ```bash -scripts/build-orient-bundle.sh "$TARGET" "$ORIENT_DIR" +scripts/build-portolan-bundle.sh "$TARGET" "$BUNDLE_DIR" ``` Hotspots use kind `config`, grouped by `surface_kind` per repo (not one hotspot per file). diff --git a/harness/recipes/deps-syft-cyclonedx.md b/harness/recipes/deps-syft-cyclonedx.md index b8d17921..98d76331 100644 --- a/harness/recipes/deps-syft-cyclonedx.md +++ b/harness/recipes/deps-syft-cyclonedx.md @@ -8,7 +8,7 @@ ```bash TARGET= -OUT=/producers/syft +OUT=/producers/syft mkdir -p "$OUT" syft scan "dir:$TARGET" -o cyclonedx-json > "$OUT/cyclonedx.json" ``` @@ -17,7 +17,7 @@ syft scan "dir:$TARGET" -o cyclonedx-json > "$OUT/cyclonedx.json" ```bash ROOT= -OUT=/producers/syft +OUT=/producers/syft mkdir -p "$OUT" while IFS= read -r repo; do name=$(basename "$repo") @@ -28,7 +28,7 @@ done < <(find "$ROOT" -name .git -type d -prune | sed 's|/.git||') ## Re-ingest ```bash -scripts/build-orient-bundle.sh "$TARGET" "$ORIENT_DIR" +scripts/build-portolan-bundle.sh "$TARGET" "$BUNDLE_DIR" ``` ## Failure modes diff --git a/harness/recipes/duplication-jscpd.md b/harness/recipes/duplication-jscpd.md index 280c2fc3..6b8ba0e1 100644 --- a/harness/recipes/duplication-jscpd.md +++ b/harness/recipes/duplication-jscpd.md @@ -9,7 +9,7 @@ ```bash TARGET= -OUT=/producers/jscpd +OUT=/producers/jscpd mkdir -p "$OUT" jscpd "$TARGET" \ --reporters json \ @@ -19,7 +19,7 @@ jscpd "$TARGET" \ --threshold 999999 \ --noSymlinks \ --gitignore \ - --ignore "**/.git/**,**/.portolan/**,**/.codex-subagents/**,**/node_modules/**,**/vendor/**,**/dist/**,**/orient-smoke/**" + --ignore "**/.git/**,**/.portolan/**,**/.codex-subagents/**,**/node_modules/**,**/vendor/**,**/dist/**,**/portolan-smoke/**" ``` ## Multi-repo (sharded) @@ -28,7 +28,7 @@ Discover git repos under the landscape root, then run one jscpd command per repo ```bash ROOT= -OUT=/producers/jscpd +OUT=/producers/jscpd mkdir -p "$OUT" while IFS= read -r repo; do name=$(basename "$repo") @@ -38,12 +38,12 @@ done < <(find "$ROOT" -name .git -type d -prune | sed 's|/.git||') ``` Failed shards do not produce duplication metrics; mark duplication `not_assessed` -for that repo in the orient bundle. +for that repo in the Portolan bundle. ## Re-ingest ```bash -scripts/build-orient-bundle.sh "$TARGET" "$ORIENT_DIR" +scripts/build-portolan-bundle.sh "$TARGET" "$BUNDLE_DIR" ``` ## Failure modes diff --git a/harness/recipes/static-semgrep-local.md b/harness/recipes/static-semgrep-local.md index 7c00b133..60503e2d 100644 --- a/harness/recipes/static-semgrep-local.md +++ b/harness/recipes/static-semgrep-local.md @@ -10,7 +10,7 @@ ```bash TARGET= RULES=/harness/recipes/semgrep-rules -OUT=/producers/semgrep +OUT=/producers/semgrep mkdir -p "$OUT" semgrep scan "$TARGET" \ --config "$RULES" \ @@ -30,7 +30,7 @@ Only after operator explicitly approves network rule sources. ## Re-ingest ```bash -scripts/build-orient-bundle.sh "$TARGET" "$ORIENT_DIR" +scripts/build-portolan-bundle.sh "$TARGET" "$BUNDLE_DIR" ``` ## Failure modes diff --git a/harness/recipes/symbols-ctags.md b/harness/recipes/symbols-ctags.md index 31394685..cdb329bf 100644 --- a/harness/recipes/symbols-ctags.md +++ b/harness/recipes/symbols-ctags.md @@ -10,7 +10,7 @@ Limit to selected repos or top-level source dirs to control output size. ```bash TARGET= -OUT=/producers/ctags +OUT=/producers/ctags mkdir -p "$OUT" # Prefer gitignore-aware file list (wizard default): git -C "$TARGET" ls-files -co --exclude-standard > /tmp/ctags-files.txt @@ -22,7 +22,7 @@ ctags --output-format=json --fields=+nKz --links=no \ ## Re-ingest ```bash -scripts/build-orient-bundle.sh "$TARGET" "$ORIENT_DIR" +scripts/build-portolan-bundle.sh "$TARGET" "$BUNDLE_DIR" ``` ## Failure modes diff --git a/internal/testfixtures/orient-bundle/orient/gaps.jsonl b/internal/testfixtures/portolan-bundle/portolan-smoke/gaps.jsonl similarity index 100% rename from internal/testfixtures/orient-bundle/orient/gaps.jsonl rename to internal/testfixtures/portolan-bundle/portolan-smoke/gaps.jsonl diff --git a/internal/testfixtures/portolan-bundle/portolan-smoke/graph-slice.json b/internal/testfixtures/portolan-bundle/portolan-smoke/graph-slice.json new file mode 100644 index 00000000..a78a19d5 --- /dev/null +++ b/internal/testfixtures/portolan-bundle/portolan-smoke/graph-slice.json @@ -0,0 +1,167 @@ +{ + "schema_version": "0.1.0", + "nodes": [ + { + "id": "sym-e92eec1034e5", + "type": "finding", + "label": "Symbol-dense file: sample.go (6 symbols)", + "kind": "debt-candidate", + "severity": "medium", + "rank": 1, + "paths": [ + "sample.go" + ] + }, + { + "id": "dup-916524ed4981", + "type": "finding", + "label": "Duplicate block (~42 lines): pkg/a/util.go", + "kind": "duplication", + "severity": "medium", + "rank": 2, + "paths": [ + "pkg/a/util.go", + "pkg/b/util.go" + ] + }, + { + "id": "dup-916524ed4981", + "type": "finding", + "label": "Duplicate block (~42 lines): pkg/a/util.go", + "kind": "duplication", + "severity": "medium", + "rank": 3, + "paths": [ + "pkg/a/util.go", + "pkg/b/util.go" + ] + }, + { + "id": "semgrep-d1e57e314224", + "type": "finding", + "label": "Semgrep portolan-hardcoded-secret-pattern", + "kind": "static-finding", + "severity": "medium", + "rank": 4, + "paths": [ + "config.env" + ] + }, + { + "id": "cfg-0b0a94188350", + "type": "finding", + "label": "Config surface: docker-compose (1 files)", + "kind": "config", + "severity": "info", + "rank": 5, + "paths": [ + "/home/fall_out_bug/projects/sdp/portolan/internal/testfixtures/portolan-bundle/target/docker-compose.yml" + ] + }, + { + "id": "cfg-41103ff5c511", + "type": "finding", + "label": "Config surface: dockerfile (1 files)", + "kind": "config", + "severity": "info", + "rank": 6, + "paths": [ + "/home/fall_out_bug/projects/sdp/portolan/internal/testfixtures/portolan-bundle/target/Dockerfile" + ] + }, + { + "id": "semgrep-47db41588c43", + "type": "finding", + "label": "Semgrep portolan-todo-fixme", + "kind": "static-finding", + "severity": "info", + "rank": 7, + "paths": [ + "sample.go" + ] + }, + { + "id": "path:/home/fall_out_bug/projects/sdp/portolan/internal/testfixtures/portolan-bundle/target/Dockerfile", + "type": "path", + "label": "Dockerfile", + "path": "/home/fall_out_bug/projects/sdp/portolan/internal/testfixtures/portolan-bundle/target/Dockerfile" + }, + { + "id": "path:/home/fall_out_bug/projects/sdp/portolan/internal/testfixtures/portolan-bundle/target/docker-compose.yml", + "type": "path", + "label": "docker-compose.yml", + "path": "/home/fall_out_bug/projects/sdp/portolan/internal/testfixtures/portolan-bundle/target/docker-compose.yml" + }, + { + "id": "path:config.env", + "type": "path", + "label": "config.env", + "path": "config.env" + }, + { + "id": "path:pkg/a/util.go", + "type": "path", + "label": "util.go", + "path": "pkg/a/util.go" + }, + { + "id": "path:pkg/b/util.go", + "type": "path", + "label": "util.go", + "path": "pkg/b/util.go" + }, + { + "id": "path:sample.go", + "type": "path", + "label": "sample.go", + "path": "sample.go" + } + ], + "edges": [ + { + "from": "path:sample.go", + "to": "sym-e92eec1034e5", + "kind": "finding_at" + }, + { + "from": "path:pkg/a/util.go", + "to": "dup-916524ed4981", + "kind": "finding_at" + }, + { + "from": "path:pkg/b/util.go", + "to": "dup-916524ed4981", + "kind": "finding_at" + }, + { + "from": "path:pkg/a/util.go", + "to": "dup-916524ed4981", + "kind": "finding_at" + }, + { + "from": "path:pkg/b/util.go", + "to": "dup-916524ed4981", + "kind": "finding_at" + }, + { + "from": "path:config.env", + "to": "semgrep-d1e57e314224", + "kind": "finding_at" + }, + { + "from": "path:/home/fall_out_bug/projects/sdp/portolan/internal/testfixtures/portolan-bundle/target/docker-compose.yml", + "to": "cfg-0b0a94188350", + "kind": "finding_at" + }, + { + "from": "path:/home/fall_out_bug/projects/sdp/portolan/internal/testfixtures/portolan-bundle/target/Dockerfile", + "to": "cfg-41103ff5c511", + "kind": "finding_at" + }, + { + "from": "path:sample.go", + "to": "semgrep-47db41588c43", + "kind": "finding_at" + } + ] +} diff --git a/internal/testfixtures/portolan-bundle/portolan-smoke/hotspots-full.jsonl b/internal/testfixtures/portolan-bundle/portolan-smoke/hotspots-full.jsonl new file mode 100644 index 00000000..9489ce9f --- /dev/null +++ b/internal/testfixtures/portolan-bundle/portolan-smoke/hotspots-full.jsonl @@ -0,0 +1,7 @@ +{"id":"sym-e92eec1034e5","kind":"debt-candidate","severity":"medium","summary":"Symbol-dense file: sample.go (6 symbols)","paths":["sample.go"],"evidence_state":"source-visible","producer":"ctags","producer_ref":"/home/fall_out_bug/projects/sdp/portolan/internal/testfixtures/portolan-bundle/portolan-smoke/producers/ctags/fixture/tags.json"} +{"id":"dup-916524ed4981","kind":"duplication","severity":"medium","summary":"Duplicate block (~42 lines): pkg/a/util.go","paths":["pkg/a/util.go","pkg/b/util.go"],"evidence_state":"metadata-visible","producer":"jscpd","producer_ref":"/home/fall_out_bug/projects/sdp/portolan/internal/testfixtures/portolan-bundle/portolan-smoke/producers/jscpd/jscpd-report.json"} +{"id":"dup-916524ed4981","kind":"duplication","severity":"medium","summary":"Duplicate block (~42 lines): pkg/a/util.go","paths":["pkg/a/util.go","pkg/b/util.go"],"evidence_state":"metadata-visible","producer":"jscpd","producer_ref":"/home/fall_out_bug/projects/sdp/portolan/internal/testfixtures/portolan-bundle/portolan-smoke/producers/jscpd/jscpd-report.json"} +{"id":"semgrep-d1e57e314224","kind":"static-finding","severity":"medium","summary":"Semgrep portolan-hardcoded-secret-pattern","paths":["config.env"],"evidence_state":"metadata-visible","producer":"semgrep","producer_ref":"/home/fall_out_bug/projects/sdp/portolan/internal/testfixtures/portolan-bundle/portolan-smoke/producers/semgrep/findings.json"} +{"id":"cfg-0b0a94188350","kind":"config","severity":"info","summary":"Config surface: docker-compose (1 files)","paths":["/home/fall_out_bug/projects/sdp/portolan/internal/testfixtures/portolan-bundle/target/docker-compose.yml"],"evidence_state":"source-visible","producer":"config-scan","producer_ref":"/home/fall_out_bug/projects/sdp/portolan/internal/testfixtures/portolan-bundle/portolan-smoke/producers/config/target-fixture.jsonl"} +{"id":"cfg-41103ff5c511","kind":"config","severity":"info","summary":"Config surface: dockerfile (1 files)","paths":["/home/fall_out_bug/projects/sdp/portolan/internal/testfixtures/portolan-bundle/target/Dockerfile"],"evidence_state":"source-visible","producer":"config-scan","producer_ref":"/home/fall_out_bug/projects/sdp/portolan/internal/testfixtures/portolan-bundle/portolan-smoke/producers/config/target-fixture.jsonl"} +{"id":"semgrep-47db41588c43","kind":"static-finding","severity":"info","summary":"Semgrep portolan-todo-fixme","paths":["sample.go"],"evidence_state":"metadata-visible","producer":"semgrep","producer_ref":"/home/fall_out_bug/projects/sdp/portolan/internal/testfixtures/portolan-bundle/portolan-smoke/producers/semgrep/findings.json"} diff --git a/internal/testfixtures/portolan-bundle/portolan-smoke/hotspots.jsonl b/internal/testfixtures/portolan-bundle/portolan-smoke/hotspots.jsonl new file mode 100644 index 00000000..74d5ef31 --- /dev/null +++ b/internal/testfixtures/portolan-bundle/portolan-smoke/hotspots.jsonl @@ -0,0 +1,7 @@ +{"id":"sym-e92eec1034e5","kind":"debt-candidate","severity":"medium","summary":"Symbol-dense file: sample.go (6 symbols)","paths":["sample.go"],"evidence_state":"source-visible","producer":"ctags","producer_ref":"/home/fall_out_bug/projects/sdp/portolan/internal/testfixtures/portolan-bundle/portolan-smoke/producers/ctags/fixture/tags.json","rank":1} +{"id":"dup-916524ed4981","kind":"duplication","severity":"medium","summary":"Duplicate block (~42 lines): pkg/a/util.go","paths":["pkg/a/util.go","pkg/b/util.go"],"evidence_state":"metadata-visible","producer":"jscpd","producer_ref":"/home/fall_out_bug/projects/sdp/portolan/internal/testfixtures/portolan-bundle/portolan-smoke/producers/jscpd/jscpd-report.json","rank":2} +{"id":"dup-916524ed4981","kind":"duplication","severity":"medium","summary":"Duplicate block (~42 lines): pkg/a/util.go","paths":["pkg/a/util.go","pkg/b/util.go"],"evidence_state":"metadata-visible","producer":"jscpd","producer_ref":"/home/fall_out_bug/projects/sdp/portolan/internal/testfixtures/portolan-bundle/portolan-smoke/producers/jscpd/jscpd-report.json","rank":3} +{"id":"semgrep-d1e57e314224","kind":"static-finding","severity":"medium","summary":"Semgrep portolan-hardcoded-secret-pattern","paths":["config.env"],"evidence_state":"metadata-visible","producer":"semgrep","producer_ref":"/home/fall_out_bug/projects/sdp/portolan/internal/testfixtures/portolan-bundle/portolan-smoke/producers/semgrep/findings.json","rank":4} +{"id":"cfg-0b0a94188350","kind":"config","severity":"info","summary":"Config surface: docker-compose (1 files)","paths":["/home/fall_out_bug/projects/sdp/portolan/internal/testfixtures/portolan-bundle/target/docker-compose.yml"],"evidence_state":"source-visible","producer":"config-scan","producer_ref":"/home/fall_out_bug/projects/sdp/portolan/internal/testfixtures/portolan-bundle/portolan-smoke/producers/config/target-fixture.jsonl","rank":5} +{"id":"cfg-41103ff5c511","kind":"config","severity":"info","summary":"Config surface: dockerfile (1 files)","paths":["/home/fall_out_bug/projects/sdp/portolan/internal/testfixtures/portolan-bundle/target/Dockerfile"],"evidence_state":"source-visible","producer":"config-scan","producer_ref":"/home/fall_out_bug/projects/sdp/portolan/internal/testfixtures/portolan-bundle/portolan-smoke/producers/config/target-fixture.jsonl","rank":6} +{"id":"semgrep-47db41588c43","kind":"static-finding","severity":"info","summary":"Semgrep portolan-todo-fixme","paths":["sample.go"],"evidence_state":"metadata-visible","producer":"semgrep","producer_ref":"/home/fall_out_bug/projects/sdp/portolan/internal/testfixtures/portolan-bundle/portolan-smoke/producers/semgrep/findings.json","rank":7} diff --git a/internal/testfixtures/portolan-bundle/portolan-smoke/landscape-card.json b/internal/testfixtures/portolan-bundle/portolan-smoke/landscape-card.json new file mode 100644 index 00000000..3feed621 --- /dev/null +++ b/internal/testfixtures/portolan-bundle/portolan-smoke/landscape-card.json @@ -0,0 +1,42 @@ +{ + "version": "0.1.0", + "scanned_at": "2026-06-11T06:31:47Z", + "identity": { + "name": "target", + "primary_language": "go", + "languages": { + "go": { + "files": 1, + "ratio": 1.0000 + }, + "config": { + "files": 1, + "ratio": 1.0000 + } + }, + "monorepo": false, + "build_system": null, + "repo_count": 1 + }, + "scale": { + "total_files": 3, + "source_files": 1, + "test_files": 0, + "test_ratio": 0.0000 + }, + "activity": { + "last_commit": null, + "contributors": 0, + "commits_30d": 0 + }, + "maturity": { + "has_readme": false, + "has_ci": false, + "has_tests": false, + "has_docker": true + }, + "health_signals": { + "staleness": "unknown", + "test_coverage_hint": "none" + } +} diff --git a/internal/testfixtures/portolan-bundle/portolan-smoke/landscape-report.json b/internal/testfixtures/portolan-bundle/portolan-smoke/landscape-report.json new file mode 100644 index 00000000..ea324927 --- /dev/null +++ b/internal/testfixtures/portolan-bundle/portolan-smoke/landscape-report.json @@ -0,0 +1,165 @@ +{ + "schema_version": "0.1.0", + "generated_at": "2026-06-11T06:31:47Z", + "target_root": "/home/fall_out_bug/projects/sdp/portolan/internal/testfixtures/portolan-bundle/target", + "sections": [ + { + "id": "overview", + "title": "Overview", + "blocks": [ + { + "type": "text", + "text": "Target: /home/fall_out_bug/projects/sdp/portolan/internal/testfixtures/portolan-bundle/target" + }, + { + "type": "text", + "text": "Findings shown: 7 of 7 found by scan" + }, + { + "type": "card_ref", + "evidence_ref": "landscape-card.json" + } + ] + }, + { + "id": "repos", + "title": "Repositories", + "items": [ + { + "id": "target", + "name": "target", + "path": "/home/fall_out_bug/projects/sdp/portolan/internal/testfixtures/portolan-bundle/target", + "evidence_ref": "repo:target" + } + ] + }, + { + "id": "findings_by_kind", + "title": "Findings", + "groups": [ + { + "kind": "config", + "count": 2, + "items": [ + { + "id": "cfg-0b0a94188350", + "summary": "Config surface: docker-compose (1 files)", + "severity": "info", + "rank": 5, + "evidence_ref": "hotspot:cfg-0b0a94188350" + }, + { + "id": "cfg-41103ff5c511", + "summary": "Config surface: dockerfile (1 files)", + "severity": "info", + "rank": 6, + "evidence_ref": "hotspot:cfg-41103ff5c511" + } + ] + }, + { + "kind": "debt-candidate", + "count": 1, + "items": [ + { + "id": "sym-e92eec1034e5", + "summary": "Symbol-dense file: sample.go (6 symbols)", + "severity": "medium", + "rank": 1, + "evidence_ref": "hotspot:sym-e92eec1034e5" + } + ] + }, + { + "kind": "duplication", + "count": 2, + "items": [ + { + "id": "dup-916524ed4981", + "summary": "Duplicate block (~42 lines): pkg/a/util.go", + "severity": "medium", + "rank": 2, + "evidence_ref": "hotspot:dup-916524ed4981" + }, + { + "id": "dup-916524ed4981", + "summary": "Duplicate block (~42 lines): pkg/a/util.go", + "severity": "medium", + "rank": 3, + "evidence_ref": "hotspot:dup-916524ed4981" + } + ] + }, + { + "kind": "static-finding", + "count": 2, + "items": [ + { + "id": "semgrep-d1e57e314224", + "summary": "Semgrep portolan-hardcoded-secret-pattern", + "severity": "medium", + "rank": 4, + "evidence_ref": "hotspot:semgrep-d1e57e314224" + }, + { + "id": "semgrep-47db41588c43", + "summary": "Semgrep portolan-todo-fixme", + "severity": "info", + "rank": 7, + "evidence_ref": "hotspot:semgrep-47db41588c43" + } + ] + } + ] + }, + { + "id": "gaps", + "title": "Not assessed", + "items": [ + { + "id": "gap-deps", + "surface": "dependencies", + "status": "not_assessed", + "summary": "No Syft/CycloneDX producer output found.", + "evidence_ref": "gap:gap-deps" + } + ] + }, + { + "id": "next_steps", + "title": "Where to look first", + "items": [ + { + "summary": "Symbol-dense file: sample.go (6 symbols)", + "kind": "debt-candidate", + "rank": 1, + "evidence_ref": "hotspot:sym-e92eec1034e5" + }, + { + "summary": "Duplicate block (~42 lines): pkg/a/util.go", + "kind": "duplication", + "rank": 2, + "evidence_ref": "hotspot:dup-916524ed4981" + }, + { + "summary": "Duplicate block (~42 lines): pkg/a/util.go", + "kind": "duplication", + "rank": 3, + "evidence_ref": "hotspot:dup-916524ed4981" + }, + { + "summary": "Semgrep portolan-hardcoded-secret-pattern", + "kind": "static-finding", + "rank": 4, + "evidence_ref": "hotspot:semgrep-d1e57e314224" + }, + { + "summary": "Config surface: docker-compose (1 files)", + "kind": "config", + "rank": 5, + "evidence_ref": "hotspot:cfg-0b0a94188350" + } + ] + } + ] +} diff --git a/internal/testfixtures/portolan-bundle/portolan-smoke/manifest.json b/internal/testfixtures/portolan-bundle/portolan-smoke/manifest.json new file mode 100644 index 00000000..670364c9 --- /dev/null +++ b/internal/testfixtures/portolan-bundle/portolan-smoke/manifest.json @@ -0,0 +1,24 @@ +{ + "schema_version": "0.1.0", + "target_root": "/home/fall_out_bug/projects/sdp/portolan/internal/testfixtures/portolan-bundle/target", + "generated_at": "2026-06-11T06:31:47Z", + "hotspot_count": 7, + "gap_count": 1, + "hotspot_budget": 200, + "hotspots_truncated": 0, + "hotspots_total": 7, + "kind_counts": { + "config": 2, + "debt-candidate": 1, + "duplication": 2, + "static-finding": 2 + }, + "kind_counts_total": { + "config": 2, + "debt-candidate": 1, + "duplication": 2, + "static-finding": 2 + }, + "gap_budget": 20, + "gaps_truncated": 0 +} diff --git a/internal/testfixtures/orient-bundle/producers/config/target-fixture.jsonl b/internal/testfixtures/portolan-bundle/portolan-smoke/producers/config/target-fixture.jsonl similarity index 100% rename from internal/testfixtures/orient-bundle/producers/config/target-fixture.jsonl rename to internal/testfixtures/portolan-bundle/portolan-smoke/producers/config/target-fixture.jsonl diff --git a/internal/testfixtures/orient-bundle/producers/ctags/fixture/tags.json b/internal/testfixtures/portolan-bundle/portolan-smoke/producers/ctags/fixture/tags.json similarity index 100% rename from internal/testfixtures/orient-bundle/producers/ctags/fixture/tags.json rename to internal/testfixtures/portolan-bundle/portolan-smoke/producers/ctags/fixture/tags.json diff --git a/internal/testfixtures/orient-bundle/orient/producers/jscpd/jscpd-report.json b/internal/testfixtures/portolan-bundle/portolan-smoke/producers/jscpd/jscpd-report.json similarity index 100% rename from internal/testfixtures/orient-bundle/orient/producers/jscpd/jscpd-report.json rename to internal/testfixtures/portolan-bundle/portolan-smoke/producers/jscpd/jscpd-report.json diff --git a/internal/testfixtures/orient-bundle/orient/producers/semgrep/findings.json b/internal/testfixtures/portolan-bundle/portolan-smoke/producers/semgrep/findings.json similarity index 100% rename from internal/testfixtures/orient-bundle/orient/producers/semgrep/findings.json rename to internal/testfixtures/portolan-bundle/portolan-smoke/producers/semgrep/findings.json diff --git a/internal/testfixtures/orient-bundle/orient/repos.json b/internal/testfixtures/portolan-bundle/portolan-smoke/repos.json similarity index 75% rename from internal/testfixtures/orient-bundle/orient/repos.json rename to internal/testfixtures/portolan-bundle/portolan-smoke/repos.json index b1f2a0ca..a6402f4b 100644 --- a/internal/testfixtures/orient-bundle/orient/repos.json +++ b/internal/testfixtures/portolan-bundle/portolan-smoke/repos.json @@ -1,7 +1,7 @@ [ { "id": "target", - "path": "/home/fall_out_bug/projects/sdp/portolan/internal/testfixtures/orient-bundle/target", + "path": "/home/fall_out_bug/projects/sdp/portolan/internal/testfixtures/portolan-bundle/target", "name": "target" } ] diff --git a/internal/testfixtures/portolan-bundle/producers/config/target-fixture.jsonl b/internal/testfixtures/portolan-bundle/producers/config/target-fixture.jsonl new file mode 100644 index 00000000..509e19c1 --- /dev/null +++ b/internal/testfixtures/portolan-bundle/producers/config/target-fixture.jsonl @@ -0,0 +1,2 @@ +{"path":"Dockerfile","surface_kind":"dockerfile"} +{"path":"docker-compose.yml","surface_kind":"docker-compose"} diff --git a/internal/testfixtures/portolan-bundle/producers/ctags/fixture/tags.json b/internal/testfixtures/portolan-bundle/producers/ctags/fixture/tags.json new file mode 100644 index 00000000..0845d4c8 --- /dev/null +++ b/internal/testfixtures/portolan-bundle/producers/ctags/fixture/tags.json @@ -0,0 +1,8 @@ +[ + {"name": "sample", "path": "sample.go", "kind": "package", "line": 1}, + {"name": "Run", "path": "sample.go", "kind": "function", "line": 4}, + {"name": "HelperA", "path": "sample.go", "kind": "function", "line": 6}, + {"name": "HelperB", "path": "sample.go", "kind": "function", "line": 7}, + {"name": "HelperC", "path": "sample.go", "kind": "function", "line": 8}, + {"name": "HelperD", "path": "sample.go", "kind": "function", "line": 9} +] diff --git a/internal/testfixtures/orient-bundle/producers/jscpd/jscpd-report.json b/internal/testfixtures/portolan-bundle/producers/jscpd/jscpd-report.json similarity index 100% rename from internal/testfixtures/orient-bundle/producers/jscpd/jscpd-report.json rename to internal/testfixtures/portolan-bundle/producers/jscpd/jscpd-report.json diff --git a/internal/testfixtures/orient-bundle/producers/semgrep/findings.json b/internal/testfixtures/portolan-bundle/producers/semgrep/findings.json similarity index 100% rename from internal/testfixtures/orient-bundle/producers/semgrep/findings.json rename to internal/testfixtures/portolan-bundle/producers/semgrep/findings.json diff --git a/internal/testfixtures/portolan-bundle/reference/gaps.jsonl b/internal/testfixtures/portolan-bundle/reference/gaps.jsonl new file mode 100644 index 00000000..904a002a --- /dev/null +++ b/internal/testfixtures/portolan-bundle/reference/gaps.jsonl @@ -0,0 +1 @@ +{"id":"gap-deps","surface":"dependencies","status":"not_assessed","summary":"No Syft/CycloneDX producer output found.","recipe":"harness/recipes/deps-syft-cyclonedx.md"} diff --git a/internal/testfixtures/orient-bundle/orient/graph-slice.json b/internal/testfixtures/portolan-bundle/reference/graph-slice.json similarity index 100% rename from internal/testfixtures/orient-bundle/orient/graph-slice.json rename to internal/testfixtures/portolan-bundle/reference/graph-slice.json diff --git a/internal/testfixtures/orient-bundle/orient/hotspots.jsonl b/internal/testfixtures/portolan-bundle/reference/hotspots.jsonl similarity index 68% rename from internal/testfixtures/orient-bundle/orient/hotspots.jsonl rename to internal/testfixtures/portolan-bundle/reference/hotspots.jsonl index 8b62a8e4..ca769775 100644 --- a/internal/testfixtures/orient-bundle/orient/hotspots.jsonl +++ b/internal/testfixtures/portolan-bundle/reference/hotspots.jsonl @@ -1,3 +1,3 @@ -{"id":"dup-916524ed4981","kind":"duplication","severity":"medium","summary":"Duplicate block (~42 lines): pkg/a/util.go","paths":["pkg/a/util.go","pkg/b/util.go"],"evidence_state":"metadata-visible","producer":"jscpd","producer_ref":"internal/testfixtures/orient-bundle/orient/producers/jscpd/jscpd-report.json","rank":1} -{"id":"semgrep-47db41588c43","kind":"static-finding","severity":"low","summary":"Semgrep portolan-todo-fixme","paths":["sample.go"],"evidence_state":"metadata-visible","producer":"semgrep","producer_ref":"internal/testfixtures/orient-bundle/orient/producers/semgrep/findings.json","rank":2} -{"id":"semgrep-d1e57e314224","kind":"static-finding","severity":"medium","summary":"Semgrep portolan-hardcoded-secret-pattern","paths":["config.env"],"evidence_state":"metadata-visible","producer":"semgrep","producer_ref":"internal/testfixtures/orient-bundle/orient/producers/semgrep/findings.json","rank":3} +{"id":"dup-916524ed4981","kind":"duplication","severity":"medium","summary":"Duplicate block (~42 lines): pkg/a/util.go","paths":["pkg/a/util.go","pkg/b/util.go"],"evidence_state":"metadata-visible","producer":"jscpd","producer_ref":"internal/testfixtures/portolan-bundle/reference/producers/jscpd/jscpd-report.json","rank":1} +{"id":"semgrep-47db41588c43","kind":"static-finding","severity":"low","summary":"Semgrep portolan-todo-fixme","paths":["sample.go"],"evidence_state":"metadata-visible","producer":"semgrep","producer_ref":"internal/testfixtures/portolan-bundle/reference/producers/semgrep/findings.json","rank":2} +{"id":"semgrep-d1e57e314224","kind":"static-finding","severity":"medium","summary":"Semgrep portolan-hardcoded-secret-pattern","paths":["config.env"],"evidence_state":"metadata-visible","producer":"semgrep","producer_ref":"internal/testfixtures/portolan-bundle/reference/producers/semgrep/findings.json","rank":3} diff --git a/internal/testfixtures/orient-bundle/orient/manifest.json b/internal/testfixtures/portolan-bundle/reference/manifest.json similarity index 80% rename from internal/testfixtures/orient-bundle/orient/manifest.json rename to internal/testfixtures/portolan-bundle/reference/manifest.json index 6f93977d..1c5834b7 100644 --- a/internal/testfixtures/orient-bundle/orient/manifest.json +++ b/internal/testfixtures/portolan-bundle/reference/manifest.json @@ -1,6 +1,6 @@ { "schema_version": "0.1.0", - "target_root": "/home/fall_out_bug/projects/sdp/portolan/internal/testfixtures/orient-bundle/target", + "target_root": "/home/fall_out_bug/projects/sdp/portolan/internal/testfixtures/portolan-bundle/target", "generated_at": "2026-06-10T13:01:32Z", "hotspot_count": 3, "gap_count": 1 diff --git a/internal/testfixtures/portolan-bundle/reference/producers/jscpd/jscpd-report.json b/internal/testfixtures/portolan-bundle/reference/producers/jscpd/jscpd-report.json new file mode 100644 index 00000000..e4420b67 --- /dev/null +++ b/internal/testfixtures/portolan-bundle/reference/producers/jscpd/jscpd-report.json @@ -0,0 +1,9 @@ +{ + "duplicates": [ + { + "firstFile": { "name": "pkg/a/util.go" }, + "secondFile": { "name": "pkg/b/util.go" }, + "lines": 42 + } + ] +} diff --git a/internal/testfixtures/portolan-bundle/reference/producers/semgrep/findings.json b/internal/testfixtures/portolan-bundle/reference/producers/semgrep/findings.json new file mode 100644 index 00000000..15786086 --- /dev/null +++ b/internal/testfixtures/portolan-bundle/reference/producers/semgrep/findings.json @@ -0,0 +1,14 @@ +{ + "results": [ + { + "check_id": "portolan-todo-fixme", + "path": "sample.go", + "extra": { "severity": "INFO", "message": "TODO marker" } + }, + { + "check_id": "portolan-hardcoded-secret-pattern", + "path": "config.env", + "extra": { "severity": "WARNING", "message": "Possible secret" } + } + ] +} diff --git a/internal/testfixtures/portolan-bundle/reference/repos.json b/internal/testfixtures/portolan-bundle/reference/repos.json new file mode 100644 index 00000000..a6402f4b --- /dev/null +++ b/internal/testfixtures/portolan-bundle/reference/repos.json @@ -0,0 +1,7 @@ +[ + { + "id": "target", + "path": "/home/fall_out_bug/projects/sdp/portolan/internal/testfixtures/portolan-bundle/target", + "name": "target" + } +] diff --git a/internal/testfixtures/orient-bundle/target/Dockerfile b/internal/testfixtures/portolan-bundle/target/Dockerfile similarity index 100% rename from internal/testfixtures/orient-bundle/target/Dockerfile rename to internal/testfixtures/portolan-bundle/target/Dockerfile diff --git a/internal/testfixtures/orient-bundle/target/docker-compose.yml b/internal/testfixtures/portolan-bundle/target/docker-compose.yml similarity index 100% rename from internal/testfixtures/orient-bundle/target/docker-compose.yml rename to internal/testfixtures/portolan-bundle/target/docker-compose.yml diff --git a/internal/testfixtures/orient-bundle/target/sample.go b/internal/testfixtures/portolan-bundle/target/sample.go similarity index 100% rename from internal/testfixtures/orient-bundle/target/sample.go rename to internal/testfixtures/portolan-bundle/target/sample.go diff --git a/scripts/build-orient-bundle.sh b/scripts/build-portolan-bundle.sh similarity index 75% rename from scripts/build-orient-bundle.sh rename to scripts/build-portolan-bundle.sh index 7376ba99..6097ebd4 100755 --- a/scripts/build-orient-bundle.sh +++ b/scripts/build-portolan-bundle.sh @@ -1,27 +1,27 @@ #!/usr/bin/env bash -# Build orient/ bundle from target root and optional producer outputs under -# /producers/. See harness/SKILL.md and spec 088/091. +# Build Portolan bundle from target root and producer outputs under +# /producers/. See harness/SKILL.md and spec 088/091/093. set -euo pipefail if [[ $# -lt 2 ]]; then - echo "usage: $0 " >&2 + echo "usage: $0 " >&2 exit 2 fi TARGET_ROOT=$(cd "$1" && pwd) -ORIENT_DIR=$2 -PRODUCERS_DIR="$ORIENT_DIR/producers" -HOTSPOT_BUDGET="${ORIENT_HOTSPOT_BUDGET:-200}" -GAP_BUDGET="${ORIENT_GAP_BUDGET:-20}" +BUNDLE_DIR=$2 +PRODUCERS_DIR="$BUNDLE_DIR/producers" +HOTSPOT_BUDGET="${PORTOLAN_HOTSPOT_BUDGET:-200}" +GAP_BUDGET="${PORTOLAN_GAP_BUDGET:-20}" if ! [[ "$HOTSPOT_BUDGET" =~ ^[0-9]+$ ]] || [[ "$HOTSPOT_BUDGET" -lt 1 ]]; then - echo "invalid ORIENT_HOTSPOT_BUDGET: $HOTSPOT_BUDGET" >&2 + echo "invalid PORTOLAN_HOTSPOT_BUDGET: $HOTSPOT_BUDGET" >&2 exit 2 fi if ! [[ "$GAP_BUDGET" =~ ^[0-9]+$ ]] || [[ "$GAP_BUDGET" -lt 1 ]]; then - echo "invalid ORIENT_GAP_BUDGET: $GAP_BUDGET" >&2 + echo "invalid PORTOLAN_GAP_BUDGET: $GAP_BUDGET" >&2 exit 2 fi -mkdir -p "$ORIENT_DIR" "$PRODUCERS_DIR" +mkdir -p "$BUNDLE_DIR" "$PRODUCERS_DIR" command -v jq >/dev/null 2>&1 || { echo "jq is required" >&2 @@ -29,8 +29,8 @@ command -v jq >/dev/null 2>&1 || { } SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) -# shellcheck source=orient-ignore.sh -. "$SCRIPT_DIR/orient-ignore.sh" +# shellcheck source=portolan-ignore.sh +. "$SCRIPT_DIR/portolan-ignore.sh" producer_repo_slug() { local p=$1 @@ -43,7 +43,7 @@ producer_repo_slug() { # --- repos.json --- if [[ -d "$TARGET_ROOT/.git" ]]; then jq -n --arg path "$TARGET_ROOT" --arg name "$(basename "$TARGET_ROOT")" \ - '[{id: $name, path: $path, name: $name}]' >"$ORIENT_DIR/repos.json" + '[{id: $name, path: $path, name: $name}]' >"$BUNDLE_DIR/repos.json" else repos='[]' while IFS= read -r gitdir; do @@ -56,7 +56,7 @@ else repos=$(jq -n --arg path "$TARGET_ROOT" --arg name "$(basename "$TARGET_ROOT")" \ '[{id: $name, path: $path, name: $name}]') fi - echo "$repos" >"$ORIENT_DIR/repos.json" + echo "$repos" >"$BUNDLE_DIR/repos.json" fi hotspots_raw=$(mktemp) @@ -83,8 +83,8 @@ bundle_path_ignored() { root="$rpath" break fi - done < <(jq -r '.[].path' "$ORIENT_DIR/repos.json") - orient_path_is_ignored "$root" "$norm" + done < <(jq -r '.[].path' "$BUNDLE_DIR/repos.json") + portolan_path_is_ignored "$root" "$norm" } filter_paths_json() { @@ -211,7 +211,7 @@ while IFS= read -r rpath; do [[ -z "$rpath" ]] && continue slug=$(producer_repo_slug "$rpath") SLUG_REPO_ROOT[$slug]="$rpath" -done < <(jq -r '.[].path' "$ORIENT_DIR/repos.json") +done < <(jq -r '.[].path' "$BUNDLE_DIR/repos.json") # --- config surfaces (producers/config/*.jsonl) --- while IFS= read -r cfile; do @@ -301,8 +301,8 @@ fi total_before=$(wc -l <"$sorted_all" | tr -d ' ') -: >"$ORIENT_DIR/hotspots-full.jsonl" -cp "$sorted_all" "$ORIENT_DIR/hotspots-full.jsonl" +: >"$BUNDLE_DIR/hotspots-full.jsonl" +cp "$sorted_all" "$BUNDLE_DIR/hotspots-full.jsonl" budgeted=$(mktemp) if [[ "$total_before" -gt "$HOTSPOT_BUDGET" ]]; then @@ -343,20 +343,20 @@ if [[ "$budget_count" -gt "$HOTSPOT_BUDGET" ]]; then truncated=1 fi -: >"$ORIENT_DIR/hotspots.jsonl" +: >"$BUNDLE_DIR/hotspots.jsonl" rank=0 while IFS= read -r line; do [[ -z "$line" ]] && continue rank=$((rank + 1)) - echo "$line" | jq -c --argjson rank "$rank" '. + {rank: $rank}' >>"$ORIENT_DIR/hotspots.jsonl" + echo "$line" | jq -c --argjson rank "$rank" '. + {rank: $rank}' >>"$BUNDLE_DIR/hotspots.jsonl" done <"$budgeted" -hotspot_count=$(wc -l <"$ORIENT_DIR/hotspots.jsonl" | tr -d ' ') +hotspot_count=$(wc -l <"$BUNDLE_DIR/hotspots.jsonl" | tr -d ' ') -kind_counts_total=$(jq -s 'group_by(.kind) | map({(.[0].kind): length}) | add // {}' "$ORIENT_DIR/hotspots-full.jsonl" 2>/dev/null || echo '{}') -kind_counts=$(jq -s 'group_by(.kind) | map({(.[0].kind): length}) | add // {}' "$ORIENT_DIR/hotspots.jsonl" 2>/dev/null || echo '{}') +kind_counts_total=$(jq -s 'group_by(.kind) | map({(.[0].kind): length}) | add // {}' "$BUNDLE_DIR/hotspots-full.jsonl" 2>/dev/null || echo '{}') +kind_counts=$(jq -s 'group_by(.kind) | map({(.[0].kind): length}) | add // {}' "$BUNDLE_DIR/hotspots.jsonl" 2>/dev/null || echo '{}') -: >"$ORIENT_DIR/gaps.jsonl" +: >"$BUNDLE_DIR/gaps.jsonl" gaps_truncated=0 if [[ -s "$gaps_raw" ]]; then gap_sorted=$(mktemp) @@ -371,14 +371,14 @@ if [[ -s "$gaps_raw" ]]; then ' "$gaps_raw" >"$gap_sorted" gap_total=$(wc -l <"$gap_sorted" | tr -d ' ') if [[ "$gap_total" -gt "$GAP_BUDGET" ]]; then - head -n "$GAP_BUDGET" "$gap_sorted" >>"$ORIENT_DIR/gaps.jsonl" + head -n "$GAP_BUDGET" "$gap_sorted" >>"$BUNDLE_DIR/gaps.jsonl" gaps_truncated=1 else - cat "$gap_sorted" >>"$ORIENT_DIR/gaps.jsonl" + cat "$gap_sorted" >>"$BUNDLE_DIR/gaps.jsonl" fi rm -f "$gap_sorted" fi -gap_count=$(wc -l <"$ORIENT_DIR/gaps.jsonl" | tr -d ' ') +gap_count=$(wc -l <"$BUNDLE_DIR/gaps.jsonl" | tr -d ' ') jq -n \ --arg schema_version "0.1.0" \ @@ -394,11 +394,88 @@ jq -n \ --argjson gap_budget "$GAP_BUDGET" \ --argjson gaps_truncated "$gaps_truncated" \ '{schema_version:$schema_version,target_root:$target_root,generated_at:$generated_at,hotspot_count:$hotspot_count,gap_count:$gap_count,hotspot_budget:$hotspot_budget,hotspots_truncated:$hotspots_truncated,hotspots_total:$hotspots_total,kind_counts:$kind_counts,kind_counts_total:$kind_counts_total,gap_budget:$gap_budget,gaps_truncated:$gaps_truncated}' \ - >"$ORIENT_DIR/manifest.json" + >"$BUNDLE_DIR/manifest.json" + +# graph-slice: findings + unique path nodes for map tab +jq -s ' + . as $hs | + ($hs | map(.paths[]? // empty) | map(select(. != "(dependency-hub)")) | unique) as $paths | + { + schema_version: "0.1.0", + nodes: ( + [ $hs[] | {id: .id, type: "finding", label: .summary, kind: .kind, severity: .severity, rank: .rank, paths: (.paths // [])} ] + + [ $paths[] | {id: ("path:" + .), type: "path", label: (. | split("/") | last), path: .} ] + ), + edges: [ $hs[] | . as $h | .paths[]? | select(. != "(dependency-hub)") | {from: ("path:" + .), to: $h.id, kind: "finding_at"} ] + } +' "$BUNDLE_DIR/hotspots.jsonl" >"$BUNDLE_DIR/graph-slice.json" 2>/dev/null || \ + echo '{"schema_version":"0.1.0","nodes":[],"edges":[]}' >"$BUNDLE_DIR/graph-slice.json" + +# landscape-card.json (spec 093) +"$SCRIPT_DIR/scan-landscape-card.sh" "$TARGET_ROOT" "$BUNDLE_DIR/landscape-card.json" || \ + echo '{"version":"0.1.0","identity":{"name":"unknown"},"scale":{},"maturity":{},"health_signals":{}}' >"$BUNDLE_DIR/landscape-card.json" -jq -s '{schema_version:"0.1.0",nodes:[.[]|{id:.id,label:.summary,kind:.kind,paths:(.paths//[])}],edges:[]}' \ - "$ORIENT_DIR/hotspots.jsonl" >"$ORIENT_DIR/graph-slice.json" 2>/dev/null || \ - echo '{"schema_version":"0.1.0","nodes":[],"edges":[]}' >"$ORIENT_DIR/graph-slice.json" +# landscape-report.json (map.md-inspired sections) +jq -n \ + --slurpfile manifest "$BUNDLE_DIR/manifest.json" \ + --slurpfile repos "$BUNDLE_DIR/repos.json" \ + --slurpfile card "$BUNDLE_DIR/landscape-card.json" \ + --rawfile hotspots "$BUNDLE_DIR/hotspots.jsonl" \ + --rawfile gaps "$BUNDLE_DIR/gaps.jsonl" \ + --rawfile hotspots_full "$BUNDLE_DIR/hotspots-full.jsonl" \ + ' + def hs_lines: ($hotspots | split("\n") | map(select(length > 0)) | map(fromjson)); + def gap_lines: ($gaps | split("\n") | map(select(length > 0)) | map(fromjson)); + def kind_groups: + hs_lines | group_by(.kind) | map({ + kind: .[0].kind, + count: length, + items: map({id: .id, summary: .summary, severity: .severity, rank: .rank, evidence_ref: ("hotspot:" + .id)}) + }); + { + schema_version: "0.1.0", + generated_at: $manifest[0].generated_at, + target_root: $manifest[0].target_root, + sections: [ + { + id: "overview", + title: "Overview", + blocks: [ + {type: "text", text: ("Target: " + $manifest[0].target_root)}, + {type: "text", text: ("Findings shown: " + ($manifest[0].hotspot_count|tostring) + " of " + ($manifest[0].hotspots_total|tostring) + " found by scan")}, + {type: "card_ref", evidence_ref: "landscape-card.json"} + ] + }, + { + id: "repos", + title: "Repositories", + items: [ $repos[0][] | {id: .id, name: .name, path: .path, evidence_ref: ("repo:" + .id)} ] + }, + { + id: "findings_by_kind", + title: "Findings", + groups: kind_groups + }, + { + id: "gaps", + title: "Not assessed", + items: [ gap_lines[] | {id: .id, surface: .surface, status: .status, summary: .summary, evidence_ref: ("gap:" + .id)} ] + }, + { + id: "next_steps", + title: "Where to look first", + items: ( + hs_lines | sort_by(.rank) | .[0:5] | map({ + summary: .summary, + kind: .kind, + rank: .rank, + evidence_ref: ("hotspot:" + .id) + }) + ) + } + ] + } + ' >"$BUNDLE_DIR/landscape-report.json" rm -f "$hotspots_raw" "$gaps_raw" "$sorted_all" "$budgeted" -echo "orient bundle written to $ORIENT_DIR (hotspots=$hotspot_count gaps=$gap_count total_before=$total_before truncated=$truncated)" +echo "Portolan bundle written to $BUNDLE_DIR (hotspots=$hotspot_count gaps=$gap_count total_before=$total_before truncated=$truncated)" diff --git a/scripts/harness-orient-smoke.sh b/scripts/harness-portolan-smoke.sh similarity index 55% rename from scripts/harness-orient-smoke.sh rename to scripts/harness-portolan-smoke.sh index 05430ec3..d96d13b4 100755 --- a/scripts/harness-orient-smoke.sh +++ b/scripts/harness-portolan-smoke.sh @@ -1,22 +1,22 @@ #!/usr/bin/env bash -# End-to-end harness orient smoke (spec 087 / 092). No network required. +# End-to-end Portolan harness smoke (spec 087 / 092 / 093). No network required. set -euo pipefail ROOT=$(cd "$(dirname "$0")/.." && pwd) -# shellcheck source=orient-ignore.sh -. "$ROOT/scripts/orient-ignore.sh" +# shellcheck source=portolan-ignore.sh +. "$ROOT/scripts/portolan-ignore.sh" -orient_rel_path_is_ignored "$ROOT" ".codex-subagents/monitor.mjs" || { +portolan_rel_path_is_ignored "$ROOT" ".codex-subagents/monitor.mjs" || { echo "expected .codex-subagents to be gitignored" >&2 exit 1 } -orient_rel_path_is_ignored "$ROOT" "scripts/orient-wizard.sh" && { - echo "did not expect scripts/orient-wizard.sh to be ignored" >&2 +portolan_rel_path_is_ignored "$ROOT" "scripts/portolan-scan.sh" && { + echo "did not expect scripts/portolan-scan.sh to be ignored" >&2 exit 1 } -FIXTURE_TARGET="$ROOT/internal/testfixtures/orient-bundle/target" -FIXTURE_ORIENT="$ROOT/internal/testfixtures/orient-bundle/orient-smoke" +FIXTURE_TARGET="$ROOT/internal/testfixtures/portolan-bundle/target" +FIXTURE_BUNDLE="$ROOT/internal/testfixtures/portolan-bundle/portolan-smoke" VIEWER_PORT="${VIEWER_PORT:-4174}" # Live config scan (producer script, not only pre-baked jsonl) @@ -26,23 +26,27 @@ jq -s 'map(select(.surface_kind == "dockerfile" and .path == "Dockerfile")) | le jq -s 'map(select(.surface_kind == "docker-compose" and .path == "docker-compose.yml")) | length >= 1' "$SCAN_TMP" >/dev/null rm -f "$SCAN_TMP" -rm -rf "$FIXTURE_ORIENT" -mkdir -p "$FIXTURE_ORIENT/producers" -cp -a "$ROOT/internal/testfixtures/orient-bundle/producers/." "$FIXTURE_ORIENT/producers/" +rm -rf "$FIXTURE_BUNDLE" +mkdir -p "$FIXTURE_BUNDLE/producers" +cp -a "$ROOT/internal/testfixtures/portolan-bundle/producers/." "$FIXTURE_BUNDLE/producers/" -"$ROOT/scripts/build-orient-bundle.sh" "$FIXTURE_TARGET" "$FIXTURE_ORIENT" +"$ROOT/scripts/build-portolan-bundle.sh" "$FIXTURE_TARGET" "$FIXTURE_BUNDLE" -test "$(wc -l <"$FIXTURE_ORIENT/hotspots.jsonl" | tr -d ' ')" -ge 1 -test -f "$FIXTURE_ORIENT/manifest.json" -test -f "$FIXTURE_ORIENT/hotspots-full.jsonl" +test "$(wc -l <"$FIXTURE_BUNDLE/hotspots.jsonl" | tr -d ' ')" -ge 1 +test -f "$FIXTURE_BUNDLE/manifest.json" +test -f "$FIXTURE_BUNDLE/hotspots-full.jsonl" +test -f "$FIXTURE_BUNDLE/landscape-card.json" +test -f "$FIXTURE_BUNDLE/landscape-report.json" +jq empty "$FIXTURE_BUNDLE/landscape-card.json" +jq empty "$FIXTURE_BUNDLE/landscape-report.json" jq -e 'select(.kind == "config" and .producer == "config-scan" and (.paths | length) >= 1)' \ - "$FIXTURE_ORIENT/hotspots.jsonl" >/dev/null || { + "$FIXTURE_BUNDLE/hotspots.jsonl" >/dev/null || { echo "expected config-scan hotspot in bundle" >&2 exit 1 } jq -e 'select(.kind == "debt-candidate" and (.summary | test("\\([0-9]+ symbols\\)")))' \ - "$FIXTURE_ORIENT/hotspots.jsonl" >/dev/null || { + "$FIXTURE_BUNDLE/hotspots.jsonl" >/dev/null || { echo "expected debt-candidate hotspot with symbol count in bundle" >&2 exit 1 } @@ -65,36 +69,39 @@ validate_gap_line() { while IFS= read -r line; do [[ -z "$line" ]] && continue echo "$line" | validate_hotspot_line || { echo "invalid hotspot record: $line" >&2; exit 1; } -done <"$FIXTURE_ORIENT/hotspots.jsonl" +done <"$FIXTURE_BUNDLE/hotspots.jsonl" -if [[ -s "$FIXTURE_ORIENT/gaps.jsonl" ]]; then +if [[ -s "$FIXTURE_BUNDLE/gaps.jsonl" ]]; then while IFS= read -r line; do [[ -z "$line" ]] && continue echo "$line" | validate_gap_line || { echo "invalid gap record: $line" >&2; exit 1; } - done <"$FIXTURE_ORIENT/gaps.jsonl" + done <"$FIXTURE_BUNDLE/gaps.jsonl" fi # Truncation smoke: budget=2 must truncate when full list is longer -TRUNC_ORIENT=$(mktemp -d) -trap 'rm -rf "$TRUNC_ORIENT"' EXIT -mkdir -p "$TRUNC_ORIENT/producers" -cp -a "$ROOT/internal/testfixtures/orient-bundle/producers/." "$TRUNC_ORIENT/producers/" -ORIENT_HOTSPOT_BUDGET=2 "$ROOT/scripts/build-orient-bundle.sh" "$FIXTURE_TARGET" "$TRUNC_ORIENT" -jq -e '.hotspots_truncated == true or .hotspots_truncated == 1' "$TRUNC_ORIENT/manifest.json" >/dev/null -full_n=$(wc -l <"$TRUNC_ORIENT/hotspots-full.jsonl" | tr -d ' ') -bud_n=$(wc -l <"$TRUNC_ORIENT/hotspots.jsonl" | tr -d ' ') +TRUNC_BUNDLE=$(mktemp -d) +trap 'rm -rf "$TRUNC_BUNDLE"' EXIT +mkdir -p "$TRUNC_BUNDLE/producers" +cp -a "$ROOT/internal/testfixtures/portolan-bundle/producers/." "$TRUNC_BUNDLE/producers/" +PORTOLAN_HOTSPOT_BUDGET=2 "$ROOT/scripts/build-portolan-bundle.sh" "$FIXTURE_TARGET" "$TRUNC_BUNDLE" +jq -e '.hotspots_truncated == true or .hotspots_truncated == 1' "$TRUNC_BUNDLE/manifest.json" >/dev/null +full_n=$(wc -l <"$TRUNC_BUNDLE/hotspots-full.jsonl" | tr -d ' ') +bud_n=$(wc -l <"$TRUNC_BUNDLE/hotspots.jsonl" | tr -d ' ') test "$full_n" -gt "$bud_n" || { echo "truncation: full=$full_n budgeted=$bud_n" >&2; exit 1; } cd "$ROOT/viewer" node scripts/build-static.js -node scripts/serve.js --bundle "$FIXTURE_ORIENT" --port "$VIEWER_PORT" & +node scripts/serve.js --bundle "$FIXTURE_BUNDLE" --port "$VIEWER_PORT" & PID=$! -trap 'kill "${PID:-}" 2>/dev/null || true; rm -rf "$TRUNC_ORIENT"' EXIT +trap 'kill "${PID:-}" 2>/dev/null || true; rm -rf "$TRUNC_BUNDLE"' EXIT sleep 1 BASE="http://127.0.0.1:$VIEWER_PORT" HTML=$(curl -sf "$BASE/") -echo "$HTML" | grep -q 'Portolan Orient' +echo "$HTML" | grep -q '

Portolan

' +echo "$HTML" | grep -q 'id="tab-bar"' +echo "$HTML" | grep -q 'id="report-overview"' +echo "$HTML" | grep -q 'id="tab-findings"' echo "$HTML" | grep -q 'id="search-input"' echo "$HTML" | grep -q 'id="filter-bar"' echo "$HTML" | grep -q 'id="heat-tree"' @@ -113,4 +120,7 @@ SYMLINK_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$BASE/source?path=leak-ou rm -f "$SYMLINK_TARGET" test "$SYMLINK_CODE" = "403" -echo "harness-orient-smoke: ok" +curl -sf "$BASE/bundle/landscape-card.json" | jq -e '.identity.name' >/dev/null +curl -sf "$BASE/bundle/landscape-report.json" | jq -e '.sections | length >= 1' >/dev/null + +echo "harness-portolan-smoke: ok" diff --git a/scripts/orient-export-from-map.sh b/scripts/portolan-export-from-map.sh similarity index 72% rename from scripts/orient-export-from-map.sh rename to scripts/portolan-export-from-map.sh index e02dbc98..75c44360 100755 --- a/scripts/orient-export-from-map.sh +++ b/scripts/portolan-export-from-map.sh @@ -1,15 +1,15 @@ #!/usr/bin/env bash -# Bridge legacy portolan map bundle -> orient/ layout. See spec 088. +# Bridge legacy portolan map bundle -> Portolan bundle layout. See spec 088. set -euo pipefail if [[ $# -lt 2 ]]; then - echo "usage: $0 " >&2 + echo "usage: $0 " >&2 exit 2 fi MAP_DIR=$1 -ORIENT_DIR=$2 -mkdir -p "$ORIENT_DIR" +BUNDLE_DIR=$2 +mkdir -p "$BUNDLE_DIR" command -v jq >/dev/null 2>&1 || { echo "jq required" >&2; exit 1; } @@ -17,10 +17,10 @@ target_root="" if [[ -f "$MAP_DIR/run.json" ]]; then target_root=$(jq -r '.root // empty' "$MAP_DIR/run.json") fi -[[ -z "$target_root" ]] && target_root=$(jq -r '.target_root // "."' "$ORIENT_DIR/manifest.json" 2>/dev/null || echo ".") +[[ -z "$target_root" ]] && target_root=$(jq -r '.target_root // "."' "$BUNDLE_DIR/manifest.json" 2>/dev/null || echo ".") rank=0 -: >"$ORIENT_DIR/hotspots.jsonl" +: >"$BUNDLE_DIR/hotspots.jsonl" if [[ -f "$MAP_DIR/findings.jsonl" ]]; then while IFS= read -r line; do [[ -z "$line" ]] && continue @@ -47,24 +47,24 @@ if [[ -f "$MAP_DIR/findings.jsonl" ]]; then producer: "portolan-map", producer_ref: "findings.jsonl", rank: $rank - }' >>"$ORIENT_DIR/hotspots.jsonl" + }' >>"$BUNDLE_DIR/hotspots.jsonl" done <"$MAP_DIR/findings.jsonl" fi -: >"$ORIENT_DIR/gaps.jsonl" +: >"$BUNDLE_DIR/gaps.jsonl" if [[ -f "$MAP_DIR/coverage.json" ]]; then jq -c '.records[]? | select(.status == "not_assessed" or .status == "unknown")' "$MAP_DIR/coverage.json" 2>/dev/null | head -20 | while read -r rec; do - echo "$rec" | jq -c '{id:("gap-" + (.surface // "unknown")),surface:(.surface // "unknown"),status:(.status // "not_assessed"),summary:(.summary // .surface)}' >>"$ORIENT_DIR/gaps.jsonl" + echo "$rec" | jq -c '{id:("gap-" + (.surface // "unknown")),surface:(.surface // "unknown"),status:(.status // "not_assessed"),summary:(.summary // .surface)}' >>"$BUNDLE_DIR/gaps.jsonl" done fi if [[ -f "$MAP_DIR/summary.json" ]]; then - cp "$MAP_DIR/summary.json" "$ORIENT_DIR/legacy-summary.json" 2>/dev/null || true + cp "$MAP_DIR/summary.json" "$BUNDLE_DIR/legacy-summary.json" 2>/dev/null || true fi -hotspot_count=$(wc -l <"$ORIENT_DIR/hotspots.jsonl" | tr -d ' ') +hotspot_count=$(wc -l <"$BUNDLE_DIR/hotspots.jsonl" | tr -d ' ') gap_count=0 -[[ -f "$ORIENT_DIR/gaps.jsonl" ]] && gap_count=$(wc -l <"$ORIENT_DIR/gaps.jsonl" | tr -d ' ') +[[ -f "$BUNDLE_DIR/gaps.jsonl" ]] && gap_count=$(wc -l <"$BUNDLE_DIR/gaps.jsonl" | tr -d ' ') jq -n \ --arg schema_version "0.1.0" \ @@ -74,15 +74,15 @@ jq -n \ --argjson gap_count "$gap_count" \ --arg source "portolan-map-bridge" \ '{schema_version:$schema_version,target_root:$target_root,generated_at:$generated_at,hotspot_count:$hotspot_count,gap_count:$gap_count,source:$source}' \ - >"$ORIENT_DIR/manifest.json" + >"$BUNDLE_DIR/manifest.json" -echo '{"schema_version":"0.1.0","repos":[]}' >"$ORIENT_DIR/repos.json" +echo '{"schema_version":"0.1.0","repos":[]}' >"$BUNDLE_DIR/repos.json" if [[ -f "$MAP_DIR/run.json" ]]; then - jq -n --arg r "$target_root" '[{id:"root",path:$r,name:( $r | split("/") | last)}]' >"$ORIENT_DIR/repos.json" + jq -n --arg r "$target_root" '[{id:"root",path:$r,name:( $r | split("/") | last)}]' >"$BUNDLE_DIR/repos.json" fi jq -s '{schema_version:"0.1.0",nodes:[.[]|{id:.id,label:.summary,kind:.kind}],edges:[]}' \ - "$ORIENT_DIR/hotspots.jsonl" >"$ORIENT_DIR/graph-slice.json" 2>/dev/null || \ - echo '{"schema_version":"0.1.0","nodes":[],"edges":[]}' >"$ORIENT_DIR/graph-slice.json" + "$BUNDLE_DIR/hotspots.jsonl" >"$BUNDLE_DIR/graph-slice.json" 2>/dev/null || \ + echo '{"schema_version":"0.1.0","nodes":[],"edges":[]}' >"$BUNDLE_DIR/graph-slice.json" -echo "exported map bundle from $MAP_DIR to $ORIENT_DIR" +echo "exported map bundle from $MAP_DIR to $BUNDLE_DIR" diff --git a/scripts/orient-ignore.sh b/scripts/portolan-ignore.sh similarity index 69% rename from scripts/orient-ignore.sh rename to scripts/portolan-ignore.sh index 76776cd3..cb21bb28 100644 --- a/scripts/orient-ignore.sh +++ b/scripts/portolan-ignore.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash -# Gitignore-aware path filtering for orient harness producers. -# Source from other scripts: . "$(dirname "$0")/orient-ignore.sh" +# Gitignore-aware path filtering for Portolan harness producers. +# Source from other scripts: . "$(dirname "$0")/portolan-ignore.sh" -orient_path_to_repo_rel() { +portolan_path_to_repo_rel() { local repo_root=$1 path=$2 repo_root=$(cd "$repo_root" && pwd) local norm @@ -22,7 +22,7 @@ orient_path_to_repo_rel() { return 1 } -orient_fallback_ignored_rel() { +portolan_fallback_ignored_rel() { local rel=$1 [[ -z "$rel" ]] && return 1 case "$rel" in @@ -32,7 +32,7 @@ orient_fallback_ignored_rel() { IFS=/ read -r -a parts <<<"$rel" for part in "${parts[@]}"; do case "$part" in - node_modules|vendor|.git|.portolan|.codex-subagents|.cursor|orient-smoke|dist|bin|generated|.DS_Store|.idea|.vscode) return 0 ;; + node_modules|vendor|.git|.portolan|.codex-subagents|.cursor|portolan-smoke|dist|bin|generated|.DS_Store|.idea|.vscode) return 0 ;; esac done case "$rel" in @@ -40,12 +40,12 @@ orient_fallback_ignored_rel() { [[ "$rel" == .agents/skills/* ]] && return 1 return 0 ;; - viewer/dist/*|*/orient-smoke/*|**/orient-smoke/**) return 0 ;; + viewer/dist/*|*/portolan-smoke/*|**/portolan-smoke/**) return 0 ;; esac return 1 } -orient_rel_path_is_ignored() { +portolan_rel_path_is_ignored() { local repo_root=$1 rel=$2 [[ -z "$rel" ]] && return 1 rel="${rel#./}" @@ -54,17 +54,17 @@ orient_rel_path_is_ignored() { return 0 fi fi - orient_fallback_ignored_rel "$rel" + portolan_fallback_ignored_rel "$rel" } -orient_path_is_ignored() { +portolan_path_is_ignored() { local repo_root=$1 path=$2 local rel - rel=$(orient_path_to_repo_rel "$repo_root" "$path" 2>/dev/null) || return 1 - orient_rel_path_is_ignored "$repo_root" "$rel" + rel=$(portolan_path_to_repo_rel "$repo_root" "$path" 2>/dev/null) || return 1 + portolan_rel_path_is_ignored "$repo_root" "$rel" } -orient_repo_file_list() { +portolan_repo_file_list() { local repo_root=$1 repo_root=$(cd "$repo_root" && pwd) if [[ -d "$repo_root/.git" ]] && command -v git >/dev/null 2>&1; then @@ -74,12 +74,12 @@ orient_repo_file_list() { while IFS= read -r f; do [[ -f "$f" ]] || continue local rel - rel=$(orient_path_to_repo_rel "$repo_root" "$f" 2>/dev/null) || continue - orient_rel_path_is_ignored "$repo_root" "$rel" && continue + rel=$(portolan_path_to_repo_rel "$repo_root" "$f" 2>/dev/null) || continue + portolan_rel_path_is_ignored "$repo_root" "$rel" && continue printf '%s\n' "$rel" done < <(find -P "$repo_root" -type f \ \( -path '*/.git/*' -o -path '*/node_modules/*' -o -path '*/vendor/*' \ - -o -path '*/.portolan/*' -o -path '*/.codex-subagents/*' -o -path '*/orient-smoke/*' \ + -o -path '*/.portolan/*' -o -path '*/.codex-subagents/*' -o -path '*/portolan-smoke/*' \ -o -path '*/dist/*' -o -path '*/.cursor/*' \) -prune \ -o -type f -print 2>/dev/null) } diff --git a/scripts/orient-wizard.sh b/scripts/portolan-scan.sh similarity index 91% rename from scripts/orient-wizard.sh rename to scripts/portolan-scan.sh index 158f174c..9687270c 100755 --- a/scripts/orient-wizard.sh +++ b/scripts/portolan-scan.sh @@ -1,13 +1,13 @@ #!/usr/bin/env bash -# One-command orient: tool check → consent install → recipes → bundle → viewer. -# See docs/specs/089-orient-wizard/ and harness/SKILL.md. +# One-command Portolan scan: tool check → recipes → bundle → viewer. +# See docs/specs/089-portolan-scan/ (superseded by portolan-scan) and harness/SKILL.md. set -euo pipefail ROOT=$(cd "$(dirname "$0")/.." && pwd) -# shellcheck source=orient-ignore.sh -. "$(dirname "$0")/orient-ignore.sh" +# shellcheck source=portolan-ignore.sh +. "$(dirname "$0")/portolan-ignore.sh" -JSCPD_IGNORE_GLOBS="**/.git/**,**/.portolan/**,**/.codex-subagents/**,**/.cursor/**,**/.agents/**,**/node_modules/**,**/vendor/**,**/build/**,**/dist/**,**/target/**,**/orient-smoke/**,**/generated/**" +JSCPD_IGNORE_GLOBS="**/.git/**,**/.portolan/**,**/.codex-subagents/**,**/.cursor/**,**/.agents/**,**/node_modules/**,**/vendor/**,**/build/**,**/dist/**,**/target/**,**/portolan-smoke/**,**/generated/**" YES=0 SKIP_INSTALL=0 @@ -21,7 +21,7 @@ JSCPD_MEMORY_MB=2048 usage() { cat <<'EOF' -usage: orient-wizard.sh [options] +usage: portolan-scan.sh [options] Options: --yes Auto-approve tool installs @@ -37,7 +37,7 @@ Options: EOF } -log() { echo "orient-wizard: $*" >&2; } +log() { echo "portolan scan: $*" >&2; } fail_log() { echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) $*" >>"$FAILURES_LOG"; } require_opt_value() { @@ -91,13 +91,13 @@ if [[ "$LIMIT_REPOS" != 0 ]] && { ! [[ "$LIMIT_REPOS" =~ ^[0-9]+$ ]] || [[ "$LIM fi TARGET_ROOT=$(cd "${POSITIONAL[0]}" && pwd) -ORIENT_DIR=${POSITIONAL[1]} -PRODUCERS_DIR="$ORIENT_DIR/producers" +BUNDLE_DIR=${POSITIONAL[1]} +PRODUCERS_DIR="$BUNDLE_DIR/producers" FAILURES_LOG="$PRODUCERS_DIR/_failures.log" SHARD_GAPS="$PRODUCERS_DIR/_gaps.jsonl" SEMGREP_RULES="$ROOT/harness/recipes/semgrep-rules/portolan-local.yaml" -mkdir -p "$ORIENT_DIR" "$PRODUCERS_DIR" +mkdir -p "$BUNDLE_DIR" "$PRODUCERS_DIR" : >"$FAILURES_LOG" : >"$SHARD_GAPS" @@ -227,7 +227,7 @@ run_ctags() { outdir="$PRODUCERS_DIR/ctags/$slug" mkdir -p "$outdir" list_file=$(mktemp) - orient_repo_file_list "$repo" >"$list_file" + portolan_repo_file_list "$repo" >"$list_file" if [[ ! -s "$list_file" ]]; then log "ctags: $repo (no non-ignored files)" rm -f "$list_file" @@ -375,24 +375,24 @@ elif has_producer ctags; then "harness/recipes/symbols-ctags.md" fi -export ORIENT_HOTSPOT_BUDGET="$HOTSPOT_BUDGET" -"$ROOT/scripts/build-orient-bundle.sh" "$TARGET_ROOT" "$ORIENT_DIR" +export PORTOLAN_HOTSPOT_BUDGET="$HOTSPOT_BUDGET" +"$ROOT/scripts/build-portolan-bundle.sh" "$TARGET_ROOT" "$BUNDLE_DIR" log "--- summary ---" -if [[ -f "$ORIENT_DIR/manifest.json" ]]; then - jq -r '"hotspots=\(.hotspot_count) gaps=\(.gap_count) target=\(.target_root)"' "$ORIENT_DIR/manifest.json" +if [[ -f "$BUNDLE_DIR/manifest.json" ]]; then + jq -r '"hotspots=\(.hotspot_count) gaps=\(.gap_count) target=\(.target_root)"' "$BUNDLE_DIR/manifest.json" fi -if [[ -f "$ORIENT_DIR/hotspots.jsonl" ]]; then +if [[ -f "$BUNDLE_DIR/hotspots.jsonl" ]]; then log "top hotspots:" - head -5 "$ORIENT_DIR/hotspots.jsonl" | jq -r '" #\(.rank) [\(.kind)] \(.summary)"' + head -5 "$BUNDLE_DIR/hotspots.jsonl" | jq -r '" #\(.rank) [\(.kind)] \(.summary)"' fi -if [[ -f "$ORIENT_DIR/gaps.jsonl" && -s "$ORIENT_DIR/gaps.jsonl" ]]; then +if [[ -f "$BUNDLE_DIR/gaps.jsonl" && -s "$BUNDLE_DIR/gaps.jsonl" ]]; then log "gaps:" - cat "$ORIENT_DIR/gaps.jsonl" | jq -r '" \(.surface): \(.status) — \(.summary)"' + cat "$BUNDLE_DIR/gaps.jsonl" | jq -r '" \(.surface): \(.status) — \(.summary)"' fi if [[ "$NO_VIEWER" -eq 1 ]]; then - log "bundle ready at $ORIENT_DIR (--no-viewer)" + log "bundle ready at $BUNDLE_DIR (--no-viewer)" exit 0 fi @@ -401,4 +401,4 @@ command -v node >/dev/null || { log "node is required for viewer"; exit 1; } cd "$ROOT/viewer" node scripts/build-static.js log "viewer: http://127.0.0.1:$PORT/ (Ctrl+C to stop)" -exec node scripts/serve.js --bundle "$ORIENT_DIR" --port "$PORT" +exec node scripts/serve.js --bundle "$BUNDLE_DIR" --port "$PORT" diff --git a/scripts/scan-config-surfaces.sh b/scripts/scan-config-surfaces.sh index c038ecce..01d5e674 100755 --- a/scripts/scan-config-surfaces.sh +++ b/scripts/scan-config-surfaces.sh @@ -4,8 +4,8 @@ set -euo pipefail SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) -# shellcheck source=orient-ignore.sh -. "$SCRIPT_DIR/orient-ignore.sh" +# shellcheck source=portolan-ignore.sh +. "$SCRIPT_DIR/portolan-ignore.sh" if [[ $# -lt 2 ]]; then echo "usage: $0 " >&2 @@ -25,7 +25,7 @@ append_surface() { local rel rel="${path#"$REPO_ROOT"/}" [[ "$rel" == "$path" && "$path" != "$REPO_ROOT"/* ]] && return 0 - orient_rel_path_is_ignored "$REPO_ROOT" "$rel" && return 0 + portolan_rel_path_is_ignored "$REPO_ROOT" "$rel" && return 0 jq -nc --arg path "$rel" --arg surface_kind "$kind" \ '{path:$path,surface_kind:$surface_kind}' >>"$OUT" } diff --git a/scripts/scan-landscape-card.sh b/scripts/scan-landscape-card.sh new file mode 100755 index 00000000..6cb791a2 --- /dev/null +++ b/scripts/scan-landscape-card.sh @@ -0,0 +1,194 @@ +#!/usr/bin/env bash +# Emit landscape-card.json (standalone Portolan contract; no sdp_lab runtime). +set -euo pipefail + +if [[ $# -lt 2 ]]; then + echo "usage: $0 " >&2 + exit 2 +fi + +TARGET_ROOT=$(cd "$1" && pwd) +OUT=$2 + +command -v jq >/dev/null || { echo "jq required" >&2; exit 1; } + +SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) +# shellcheck source=portolan-ignore.sh +. "$SCRIPT_DIR/portolan-ignore.sh" + +scan_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ") +name=$(basename "$TARGET_ROOT") + +# Language detection from source extensions (git ls-files when possible) +declare -A lang_files +total_files=0 +source_files=0 +test_files=0 + +count_file() { + local rel=$1 + local ext="${rel##*.}" + case "$ext" in + go) lang_files[go]=$(( ${lang_files[go]:-0} + 1 )) ; source_files=$((source_files + 1)) ;; + ts|tsx) lang_files[typescript]=$(( ${lang_files[typescript]:-0} + 1 )) ; source_files=$((source_files + 1)) ;; + js|jsx|mjs|cjs) lang_files[javascript]=$(( ${lang_files[javascript]:-0} + 1 )) ; source_files=$((source_files + 1)) ;; + py) lang_files[python]=$(( ${lang_files[python]:-0} + 1 )) ; source_files=$((source_files + 1)) ;; + sh|bash) lang_files[shell]=$(( ${lang_files[shell]:-0} + 1 )) ; source_files=$((source_files + 1)) ;; + md) lang_files[markdown]=$(( ${lang_files[markdown]:-0} + 1 )) ;; + json|jsonl|yaml|yml) lang_files[config]=$(( ${lang_files[config]:-0} + 1 )) ;; + *) ;; + esac + if [[ "$rel" == *test* || "$rel" == *_test.go ]]; then + test_files=$((test_files + 1)) + fi + total_files=$((total_files + 1)) +} + +if [[ -d "$TARGET_ROOT/.git" ]]; then + while IFS= read -r rel; do + [[ -z "$rel" ]] && continue + portolan_rel_path_is_ignored "$TARGET_ROOT" "$rel" && continue + count_file "$rel" + done < <(portolan_repo_file_list "$TARGET_ROOT") +else + while IFS= read -r f; do + rel=${f#"$TARGET_ROOT"/} + count_file "$rel" + done < <(find "$TARGET_ROOT" -type f 2>/dev/null | head -5000) +fi + +primary_lang="unknown" +max_count=0 +for lang in "${!lang_files[@]}"; do + c=${lang_files[$lang]} + if [[ $c -gt $max_count ]]; then + max_count=$c + primary_lang=$lang + fi +done + +languages_json='{}' +for lang in "${!lang_files[@]}"; do + ratio=0 + if [[ $source_files -gt 0 ]]; then + ratio=$(awk "BEGIN {printf \"%.4f\", ${lang_files[$lang]} / $source_files}") + fi + languages_json=$(echo "$languages_json" | jq --arg l "$lang" --argjson f "${lang_files[$lang]}" --argjson r "$ratio" \ + '. + {($l): {files: $f, ratio: ($r | tonumber)}}') +done + +test_ratio=0 +if [[ $source_files -gt 0 ]]; then + test_ratio=$(awk "BEGIN {printf \"%.4f\", $test_files / $source_files}") +fi + +# Git activity +last_commit=null +contributors=0 +commits_30d=0 +if [[ -d "$TARGET_ROOT/.git" ]]; then + last_commit=$(git -C "$TARGET_ROOT" log -1 --format=%cs 2>/dev/null || echo null) + contributors=$(git -C "$TARGET_ROOT" shortlog -sn --all 2>/dev/null | wc -l | tr -d ' ') + commits_30d=$(git -C "$TARGET_ROOT" log --since=30.days.ago --oneline 2>/dev/null | wc -l | tr -d ' ') +fi + +staleness="unknown" +if [[ "$last_commit" != "null" && -n "$last_commit" ]]; then + days_ago=$(( ($(date +%s) - $(date -d "$last_commit" +%s 2>/dev/null || echo 0)) / 86400 )) + if [[ $days_ago -lt 30 ]]; then staleness="active" + elif [[ $days_ago -lt 180 ]]; then staleness="recent" + elif [[ $days_ago -lt 365 ]]; then staleness="stale" + else staleness="dormant" + fi +fi + +has_readme=false +has_ci=false +has_tests=false +has_docker=false +[[ -f "$TARGET_ROOT/README.md" || -f "$TARGET_ROOT/readme.md" ]] && has_readme=true +[[ -d "$TARGET_ROOT/.github/workflows" ]] && has_ci=true +[[ $test_files -gt 0 ]] && has_tests=true +[[ -f "$TARGET_ROOT/Dockerfile" || -f "$TARGET_ROOT/docker-compose.yml" ]] && has_docker=true + +monorepo=false +repo_count=0 +if [[ -d "$TARGET_ROOT/.git" ]]; then + repo_count=1 +else + while IFS= read -r gitdir; do + repo_count=$((repo_count + 1)) + done < <(find "$TARGET_ROOT" -maxdepth 4 -type d -name .git 2>/dev/null) + [[ $repo_count -gt 1 ]] && monorepo=true +fi +[[ $repo_count -eq 0 ]] && repo_count=1 + +build_system=null +for f in go.mod package.json Cargo.toml pom.xml build.gradle; do + if [[ -f "$TARGET_ROOT/$f" ]]; then + case "$f" in + go.mod) build_system="go" ;; + package.json) build_system="npm" ;; + Cargo.toml) build_system="cargo" ;; + pom.xml|build.gradle) build_system="maven" ;; + esac + break + fi +done + +jq -n \ + --arg version "0.1.0" \ + --arg scanned_at "$scan_at" \ + --arg name "$name" \ + --arg primary_language "$primary_lang" \ + --argjson languages "$languages_json" \ + --argjson monorepo "$monorepo" \ + --arg build_system "${build_system:-null}" \ + --argjson total_files "$total_files" \ + --argjson source_files "$source_files" \ + --argjson test_files "$test_files" \ + --argjson test_ratio "$test_ratio" \ + --arg last_commit "${last_commit:-null}" \ + --argjson contributors "$contributors" \ + --argjson commits_30d "$commits_30d" \ + --argjson has_readme "$has_readme" \ + --argjson has_ci "$has_ci" \ + --argjson has_tests "$has_tests" \ + --argjson has_docker "$has_docker" \ + --arg staleness "$staleness" \ + --argjson repo_count "$repo_count" \ + '{ + version: $version, + scanned_at: $scanned_at, + identity: { + name: $name, + primary_language: $primary_language, + languages: $languages, + monorepo: $monorepo, + build_system: (if $build_system == "null" then null else $build_system end), + repo_count: $repo_count + }, + scale: { + total_files: $total_files, + source_files: $source_files, + test_files: $test_files, + test_ratio: $test_ratio + }, + activity: { + last_commit: (if $last_commit == "null" then null else $last_commit end), + contributors: $contributors, + commits_30d: $commits_30d + }, + maturity: { + has_readme: $has_readme, + has_ci: $has_ci, + has_tests: $has_tests, + has_docker: $has_docker + }, + health_signals: { + staleness: $staleness, + test_coverage_hint: (if $test_ratio > 0.3 then "good" elif $test_ratio > 0.1 then "partial" elif $test_files > 0 then "low" else "none" end) + } + }' >"$OUT" + +echo "landscape-card: $OUT" >&2 diff --git a/viewer/README.md b/viewer/README.md index 36e529da..38606d86 100644 --- a/viewer/README.md +++ b/viewer/README.md @@ -1,16 +1,16 @@ -# Portolan Orient Viewer +# Portolan Viewer -UA-inspired local map for evidence-backed hotspots. Loads `orient/` bundle only; -does not accept LLM-generated graphs as truth. +Local UI for ranked scanner hotspots and a folder tree. Loads a Portolan bundle +(`manifest.json`, `hotspots.jsonl`, …) only — no LLM-generated graphs. ## Build and serve ```bash node scripts/build-static.js -node scripts/serve.js --bundle /path/to/orient +node scripts/serve.js --bundle /path/to/bundle ``` -Or with npm when available: `npm run build && npm run serve -- --bundle /path/to/orient` +Or with npm when available: `npm run build && npm run serve -- --bundle /path/to/bundle` Open http://127.0.0.1:4173/ @@ -18,7 +18,9 @@ Open http://127.0.0.1:4173/ ```bash npm run build -npm run serve -- --bundle ../internal/testfixtures/orient-bundle/orient +npm run serve -- --bundle ../internal/testfixtures/portolan-bundle/portolan-smoke ``` +Bundle from `build-portolan-bundle.sh` also includes `landscape-card.json` and `landscape-report.json` for the Overview tab. + Read-only, local-only, stops when the server process exits. diff --git a/viewer/package.json b/viewer/package.json index 145ae9f3..f724f080 100644 --- a/viewer/package.json +++ b/viewer/package.json @@ -1,8 +1,8 @@ { - "name": "portolan-orient-viewer", + "name": "portolan-viewer", "version": "0.1.0", "private": true, - "description": "Local evidence-backed orient map viewer (UA-inspired, no LLM graph truth)", + "description": "Local viewer for Portolan hotspot bundles (folder tree + ranked list)", "scripts": { "build": "node scripts/build-static.js", "serve": "node scripts/serve.js" diff --git a/viewer/scripts/serve.js b/viewer/scripts/serve.js index 0babee20..97843912 100644 --- a/viewer/scripts/serve.js +++ b/viewer/scripts/serve.js @@ -16,7 +16,7 @@ for (let i = 0; i < args.length; i++) { } if (!bundlePath || !fs.existsSync(bundlePath)) { - console.error('usage: npm run serve -- --bundle [--port 4173]'); + console.error('usage: npm run serve -- --bundle [--port 4173]'); process.exit(2); } @@ -146,6 +146,15 @@ const server = http.createServer((req, res) => { if (url.pathname === '/bundle/repos.json') { return sendFile(path.join(bundlePath, 'repos.json'), res); } + if (url.pathname === '/bundle/hotspots-full.jsonl') { + return sendFile(path.join(bundlePath, 'hotspots-full.jsonl'), res); + } + if (url.pathname === '/bundle/landscape-card.json') { + return sendFile(path.join(bundlePath, 'landscape-card.json'), res); + } + if (url.pathname === '/bundle/landscape-report.json') { + return sendFile(path.join(bundlePath, 'landscape-report.json'), res); + } const distResolved = path.resolve(distDir) + path.sep; let filePath = path.resolve( @@ -172,6 +181,6 @@ function sendFile(filePath, res) { } server.listen(port, '127.0.0.1', () => { - console.log(`Portolan orient viewer: http://127.0.0.1:${port}/`); + console.log(`Portolan viewer: http://127.0.0.1:${port}/`); console.log(`Bundle: ${bundlePath}`); }); diff --git a/viewer/src/app.js b/viewer/src/app.js index e9fe7c09..01ceb394 100644 --- a/viewer/src/app.js +++ b/viewer/src/app.js @@ -1,17 +1,89 @@ /** - * Portolan orient viewer — evidence bundle from /bundle/*; UA-inspired navigation. + * Portolan viewer — ranked hotspots and folder tree from /bundle/* (evidence only). */ const SEV_RANK = { critical: 0, high: 1, medium: 2, low: 3, info: 4 }; +const KIND_LABELS = { + duplication: 'Duplication', + 'static-finding': 'Static smell', + config: 'Config surface', + 'debt-candidate': 'Symbol density', + 'dep-hub': 'Dependency hub', +}; + +/** viewMode: preset id or 'custom' when user toggles kind chips */ +const VIEWS = { + all: { + label: 'All', + description: 'Every hotspot in this bundle (subject to search & severity filters).', + }, + top15: { + label: 'Tour · top 15', + description: + 'Only ranks #1–#15. Rank is bundle sort order (severity first, then kind quotas) — a guided slice, not the full landscape.', + maxRank: 15, + }, + 'code-pain': { + label: 'Code pain', + description: 'Duplication, semgrep smells, and symbol-dense files — typical refactor/review targets.', + kinds: ['duplication', 'static-finding', 'debt-candidate'], + }, + config: { + label: 'Config & deploy', + description: 'Docker, CI, compose, env, terraform inventory — where ops/config risk lives (informational).', + kinds: ['config'], + }, + deps: { + label: 'Dependencies', + description: 'Packages with many dependents in the SBOM (Syft) — upgrade blast-radius hubs.', + kinds: ['dep-hub'], + }, +}; + +const KIND_HELP = { + duplication: { + why: 'jscpd found a repeated code block. Copies drift apart when you change one copy and forget the other.', + tool: 'jscpd', + limit: 'Only near-identical fragments; not design-level duplication.', + }, + 'static-finding': { + why: 'Semgrep matched a local rule (TODO/FIXME, secret-like patterns, etc.).', + tool: 'semgrep', + limit: 'Rule-based; not a full security audit.', + }, + config: { + why: 'File is part of deploy/config surface (Dockerfile, workflow, .env, terraform…).', + tool: 'config scan', + limit: 'Inventory only — severity is info, not a vulnerability claim.', + }, + 'debt-candidate': { + why: 'File has many symbols (ctags). Dense files are harder to navigate — candidate for split or review.', + tool: 'universal-ctags', + limit: 'Symbol count ≠ complexity score; no AI judgment.', + }, + 'dep-hub': { + why: 'Package has many dependency edges in the CycloneDX SBOM.', + tool: 'syft', + limit: 'Landscape-level; no file path attached.', + }, +}; + let allHotspots = []; let repos = []; let manifest = null; let gaps = []; +let landscapeCard = null; +let landscapeReport = null; let filters = { kinds: new Set(), severities: new Set(), repoIds: new Set() }; let searchQuery = ''; let selectedId = null; let expandedDirs = new Set(); +/** @type {'overview' | 'findings' | 'gaps'} */ +let activeTab = 'overview'; +let usingFullHotspots = false; +/** @type {keyof VIEWS | 'custom'} */ +let viewMode = 'top15'; async function loadJSONL(url) { const res = await fetch(url); @@ -37,6 +109,17 @@ function escapeHtml(s) { .replace(/>/g, '>'); } +function escapeAttr(s) { + return escapeHtml(s) + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +function safeRank(rank) { + const n = Number(rank); + return Number.isFinite(n) ? String(n) : '?'; +} + function sevRank(s) { return SEV_RANK[s] ?? 5; } @@ -46,11 +129,46 @@ function maxSeverity(sevs) { } function sevClass(s) { - return `sev-${s || 'info'}`; + if (s && Object.prototype.hasOwnProperty.call(SEV_RANK, s)) return `sev-${s}`; + return 'sev-info'; +} + +function safeKindClass(kind) { + if (kind && Object.prototype.hasOwnProperty.call(KIND_LABELS, kind)) return kind; + return 'unknown-kind'; +} + +function kindLabel(kind) { + return KIND_LABELS[kind] || kind; } -function kindClass(kind) { - return kind || 'debt-candidate'; +const KIND_SECTION_ORDER = [ + 'duplication', + 'static-finding', + 'config', + 'debt-candidate', + 'dep-hub', +]; + +function findingsShownCount() { + if (usingFullHotspots) return allHotspots.length; + return manifest?.hotspot_count ?? allHotspots.length; +} + +function findingsTotalCount() { + if (usingFullHotspots) return allHotspots.length; + return manifest?.hotspots_total ?? allHotspots.length; +} + +function updateManifestFooter() { + const el = document.getElementById('manifest-info'); + if (!manifest) { + el.textContent = 'no manifest'; + return; + } + const targetShort = shortPath(manifest.target_root) || manifest.target_root; + el.textContent = + `${targetShort} · ${findingsShownCount()}/${findingsTotalCount()} findings · ${manifest.gap_count} gaps`; } function normalizeDisplayPath(p) { @@ -58,6 +176,32 @@ function normalizeDisplayPath(p) { return p.replace(/\\/g, '/'); } +function shortPath(p) { + const norm = normalizeDisplayPath(p); + if (!norm || norm === '(dependency-hub)') return norm; + if (norm.startsWith('/')) { + for (const r of repos) { + const root = r.path.replace(/\\/g, '/'); + if (norm === root || norm.startsWith(root + '/')) { + const rel = norm.slice(root.length).replace(/^\//, ''); + return rel || r.name || r.id; + } + } + const parts = norm.split('/').filter(Boolean); + if (parts.length <= 2) return parts.join('/'); + return '…/' + parts.slice(-2).join('/'); + } + const parts = norm.split('/').filter(Boolean); + if (parts.length <= 3) return norm; + return '…/' + parts.slice(-2).join('/'); +} + +function primaryPath(h) { + const paths = h.paths || []; + if (!paths.length) return null; + return paths.find((p) => p && p !== '(dependency-hub)') || paths[0]; +} + function repoForPath(p) { const norm = normalizeDisplayPath(p); if (!norm) return null; @@ -86,25 +230,28 @@ function hotspotRepo(h) { function matchesSearch(h, q) { if (!q) return true; - const hay = [ - h.summary, - h.id, - h.kind, - h.severity, - ...(h.paths || []), - ] + const hay = [h.summary, h.id, h.kind, h.severity, kindLabel(h.kind), ...(h.paths || [])] .join(' ') .toLowerCase(); return hay.includes(q); } +function matchesView(h) { + if (viewMode === 'custom') { + if (filters.kinds.size && !filters.kinds.has(h.kind)) return false; + return true; + } + const view = VIEWS[viewMode]; + if (!view) return true; + if (view.kinds && !view.kinds.includes(h.kind)) return false; + if (view.maxRank != null && h.rank > view.maxRank) return false; + return true; +} + function matchesFilters(h) { - if (filters.kinds.size && !filters.kinds.has(h.kind)) return false; if (filters.severities.size && !filters.severities.has(h.severity)) return false; if (filters.repoIds.size) { - if (h.kind === 'dep-hub') { - // Dependency hubs are landscape-wide; repo chips should not hide them. - } else { + if (h.kind !== 'dep-hub') { const rid = hotspotRepo(h); if (!rid || !filters.repoIds.has(rid)) return false; } @@ -115,10 +262,118 @@ function matchesFilters(h) { function filteredHotspots() { const q = searchQuery.trim().toLowerCase(); return allHotspots - .filter((h) => matchesSearch(h, q) && matchesFilters(h)) + .filter((h) => matchesSearch(h, q) && matchesView(h) && matchesFilters(h)) .sort((a, b) => a.rank - b.rank); } +function kindCounts(hotspots) { + const counts = {}; + for (const h of hotspots) { + counts[h.kind] = (counts[h.kind] || 0) + 1; + } + return counts; +} + +function setView(mode) { + viewMode = mode; + filters.kinds.clear(); + renderViewBar(); + renderFilterExplainer(); + renderFilters(); + render(); +} + +function enterCustomKindFilter(kind) { + viewMode = 'custom'; + filters.kinds.clear(); + filters.kinds.add(kind); + renderViewBar(); + renderFilterExplainer(); + renderFilters(); + render(); +} + +function toggleCustomKind(kind) { + if (filters.kinds.has(kind)) filters.kinds.delete(kind); + else filters.kinds.add(kind); + renderFilterExplainer(); + renderFilters(); + render(); +} + +function kindChipState(kind) { + if (viewMode === 'custom') { + if (filters.kinds.size === 0) return 'inactive'; + return filters.kinds.has(kind) ? 'active' : 'inactive'; + } + const view = VIEWS[viewMode]; + if (view?.kinds) { + return view.kinds.includes(kind) ? 'locked' : 'inactive'; + } + return 'inactive'; +} + +function renderViewBar() { + const el = document.getElementById('view-bar'); + el.innerHTML = ''; + for (const [id, view] of Object.entries(VIEWS)) { + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'view-btn' + (viewMode === id ? ' active' : ''); + btn.textContent = view.label; + btn.title = view.description; + btn.addEventListener('click', () => setView(id)); + el.appendChild(btn); + } + if (viewMode === 'custom') { + const custom = document.createElement('span'); + custom.className = 'view-btn active custom-tag'; + custom.textContent = 'Custom kind filter'; + el.appendChild(custom); + } +} + +function renderFilterExplainer() { + const el = document.getElementById('filter-explainer'); + const shown = filteredHotspots().length; + const total = allHotspots.length; + let main = ''; + if (viewMode === 'custom') { + const kinds = + filters.kinds.size === 0 + ? 'all kinds' + : [...filters.kinds].map(kindLabel).join(', '); + main = `Custom filter — kinds: ${escapeHtml(kinds)}. Click kind chips below to add/remove. Pick a view above to reset.`; + } else { + const view = VIEWS[viewMode]; + main = view + ? `${escapeHtml(view.label)} — ${escapeHtml(view.description)}` + : ''; + } + el.innerHTML = ` +

${main}

+

Showing ${shown} of ${total} hotspots in bundle. + Rank #n = sort position (severity + per-kind budget). + Only tools that ran in this scan appear; missing layers are gaps, not hidden hotspots.

+ `; +} + +function renderKindGuide() { + const el = document.getElementById('kind-guide-body'); + const order = ['duplication', 'static-finding', 'debt-candidate', 'config', 'dep-hub']; + el.innerHTML = order + .filter((k) => KIND_HELP[k]) + .map((k) => { + const h = KIND_HELP[k]; + return `
+

${escapeHtml(kindLabel(k))}

+

${escapeHtml(h.why)}

+

Tool: ${escapeHtml(h.tool)} · ${escapeHtml(h.limit)}

+
`; + }) + .join(''); +} + function buildTree(hotspots) { const root = { name: '', @@ -143,12 +398,17 @@ function buildTree(hotspots) { function addPath(pathStr, h) { const norm = normalizeDisplayPath(pathStr); - if (!norm || norm === '(dependency-hubs)') { + if (!norm || norm === '(dependency-hub)') { const node = ensureChild(root, '(dependency-hubs)', '(dependency-hubs)', false); node.hotspotIds.add(h.id); return; } - const parts = norm.split('/').filter(Boolean); + let rel = norm; + if (norm.startsWith('/')) { + rel = shortPath(norm); + if (rel.startsWith('…/')) rel = norm.split('/').pop() || norm; + } + const parts = rel.split('/').filter(Boolean); let node = root; let built = ''; for (let i = 0; i < parts.length; i++) { @@ -162,7 +422,7 @@ function buildTree(hotspots) { for (const h of hotspots) { if (!h.paths || h.paths.length === 0) { - addPath('(dependency-hubs)', h); + addPath('(dependency-hub)', h); } else { for (const p of h.paths) addPath(p, h); } @@ -179,14 +439,44 @@ function treeStats(node, hotspotById) { function renderFilters() { const bar = document.getElementById('filter-bar'); bar.innerHTML = ''; - + const counts = kindCounts(allHotspots); const kinds = [...new Set(allHotspots.map((h) => h.kind))].sort(); const severities = [...new Set(allHotspots.map((h) => h.severity))].sort( (a, b) => sevRank(a) - sevRank(b) ); - bar.appendChild(makeChipGroup('Kind', kinds, filters.kinds, () => render())); - bar.appendChild(makeChipGroup('Severity', severities, filters.severities, () => render())); + const kindWrap = document.createElement('div'); + kindWrap.className = 'filter-group'; + const kindLbl = document.createElement('span'); + kindLbl.className = 'filter-label'; + kindLbl.textContent = viewMode === 'custom' ? 'Kind (custom)' : 'Kind (click to customize)'; + kindWrap.appendChild(kindLbl); + for (const v of kinds) { + const state = kindChipState(v); + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'chip ' + state; + btn.innerHTML = + escapeHtml(kindLabel(v)) + `${counts[v] ?? 0}`; + if (state === 'locked') { + btn.title = 'Set by current view. Click to switch to custom filter for this kind only.'; + } else if (state === 'inactive') { + btn.title = 'Click to filter by this kind only (switches to custom filter).'; + } + btn.addEventListener('click', () => { + if (viewMode !== 'custom') { + enterCustomKindFilter(v); + } else { + toggleCustomKind(v); + } + }); + kindWrap.appendChild(btn); + } + bar.appendChild(kindWrap); + + bar.appendChild( + makeChipGroup('Severity', severities, filters.severities, () => render(), (v) => v, null) + ); if (repos.length > 1) { const repoLabels = repos.map((r) => ({ id: r.id, label: r.name || r.id })); bar.appendChild( @@ -195,13 +485,14 @@ function renderFilters() { repoLabels.map((r) => r.id), filters.repoIds, () => render(), - (id) => repoLabels.find((r) => r.id === id)?.label || id + (id) => repoLabels.find((r) => r.id === id)?.label || id, + null ) ); } } -function makeChipGroup(label, values, activeSet, onChange, labelFn = (v) => v) { +function makeChipGroup(label, values, activeSet, onChange, labelFn = (v) => v, counts = null) { const wrap = document.createElement('div'); wrap.className = 'filter-group'; const lbl = document.createElement('span'); @@ -212,7 +503,9 @@ function makeChipGroup(label, values, activeSet, onChange, labelFn = (v) => v) { const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'chip' + (activeSet.has(v) ? ' active' : ''); - btn.textContent = labelFn(v); + const count = counts?.[v]; + btn.innerHTML = + escapeHtml(labelFn(v)) + (count != null ? `${count}` : ''); btn.addEventListener('click', () => { if (activeSet.has(v)) activeSet.delete(v); else activeSet.add(v); @@ -226,14 +519,20 @@ function makeChipGroup(label, values, activeSet, onChange, labelFn = (v) => v) { function renderBanner() { const el = document.getElementById('status-banner'); const parts = []; - if (manifest?.hotspots_truncated) { + if (manifest?.hotspots_truncated && !usingFullHotspots) { parts.push( - `Showing ${manifest.hotspot_count} of ${manifest.hotspots_total} hotspots (budget ${manifest.hotspot_budget} applied).` + `Bundle truncated: ${manifest.hotspot_count} of ${manifest.hotspots_total} hotspots (budget ${manifest.hotspot_budget}). Full list: hotspots-full.jsonl.` ); } if (gaps.length) { - const summary = gaps.map((g) => `${g.surface}: ${g.status}`).join(' · '); - parts.push(`Gaps: ${escapeHtml(summary)}`); + const items = gaps + .slice(0, 4) + .map((g) => `${escapeHtml(g.surface)}: ${escapeHtml(g.status)}`) + .join(' · '); + const more = gaps.length > 4 ? ` (+${gaps.length - 4} more)` : ''; + parts.push( + `Layers not run or empty: ${items}${more}. These are not hidden hotspots — they were not assessed in this run.` + ); } if (parts.length) { el.innerHTML = parts.join(' '); @@ -244,15 +543,49 @@ function renderBanner() { } } -function renderTour(hotspots, hotspotById) { +function kindWhyLine(kind) { + const h = KIND_HELP[kind]; + return h ? h.why : 'Flagged by a local scanner in this Portolan bundle.'; +} + +function renderTour(hotspots) { const list = document.getElementById('hotspot-list'); list.innerHTML = ''; - document.getElementById('tour-count').textContent = `(${hotspots.length})`; + document.getElementById('tour-count').textContent = + hotspots.length === allHotspots.length + ? `${hotspots.length} shown` + : `${hotspots.length} of ${allHotspots.length}`; + + if (!hotspots.length) { + const empty = document.createElement('li'); + empty.className = 'detail-empty'; + empty.innerHTML = + '

No matches

Try view All or clear severity filters.

'; + list.appendChild(empty); + return; + } + for (const h of hotspots) { const li = document.createElement('li'); - li.className = h.id === selectedId ? 'active' : ''; - li.innerHTML = `${escapeHtml(h.severity)}${escapeHtml(h.summary)}`; - li.addEventListener('click', () => selectHotspot(h)); + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'hotspot-card' + (h.id === selectedId ? ' active' : ''); + const path = primaryPath(h); + const pathHtml = path + ? `
${escapeHtml(shortPath(path))}
` + : ''; + btn.innerHTML = ` +
+ #${safeRank(h.rank)} + ${escapeHtml(kindLabel(h.kind))} + ${escapeHtml(h.severity in SEV_RANK ? h.severity : 'info')} +
+

${escapeHtml(h.summary)}

+

${escapeHtml(kindWhyLine(h.kind))}

+ ${pathHtml} + `; + btn.addEventListener('click', () => selectHotspot(h)); + li.appendChild(btn); list.appendChild(li); } } @@ -266,7 +599,7 @@ function renderTreeNode(node, hotspotById, parentEl, depth = 0) { for (const [, child] of entries) { const stats = treeStats(child, hotspotById); const hasChildren = child.children.size > 0; - const isExpanded = expandedDirs.has(child.pathKey) || depth < 2; + const isExpanded = expandedDirs.has(child.pathKey) || depth < 1; const row = document.createElement('div'); row.className = 'tree-node'; @@ -277,7 +610,7 @@ function renderTreeNode(node, hotspotById, parentEl, depth = 0) { toggle.className = 'tree-toggle' + (hasChildren ? '' : ' empty'); toggle.textContent = hasChildren ? (isExpanded ? '▾' : '▸') : ''; const bar = document.createElement('span'); - bar.className = `sev-bar ${child.isFile ? sevClass(stats.maxSeverity) : sevClass(stats.maxSeverity)}`; + bar.className = `sev-bar ${sevClass(stats.maxSeverity)}`; const name = document.createElement('span'); name.className = 'tree-name'; name.textContent = child.name; @@ -308,7 +641,8 @@ function renderTreeNode(node, hotspotById, parentEl, depth = 0) { for (const h of fileHotspots) { const hs = document.createElement('div'); hs.className = 'tree-hotspot' + (h.id === selectedId ? ' active' : ''); - hs.innerHTML = `${escapeHtml(h.kind)}#${h.rank} ${escapeHtml(h.summary)}`; + hs.textContent = `#${h.rank} ${kindLabel(h.kind)}`; + hs.title = h.summary; hs.addEventListener('click', () => selectHotspot(h)); row.appendChild(hs); } @@ -320,12 +654,18 @@ function renderTreeNode(node, hotspotById, parentEl, depth = 0) { row.appendChild(childWrap); parentEl.appendChild(row); } - } function renderTree(hotspots) { const treeEl = document.getElementById('heat-tree'); + const countEl = document.getElementById('tree-count'); treeEl.innerHTML = ''; + if (countEl) { + countEl.textContent = + hotspots.length === allHotspots.length + ? `${hotspots.length} hotspots` + : `${hotspots.length} of ${allHotspots.length}`; + } const hotspotById = new Map(hotspots.map((h) => [h.id, h])); const root = buildTree(hotspots); @@ -334,14 +674,15 @@ function renderTree(hotspots) { const stats = treeStats(depNode, hotspotById); const row = document.createElement('div'); row.className = 'tree-row'; - row.innerHTML = `(dependency-hubs)${stats.count}`; + row.innerHTML = `(dependency-hubs)${stats.count}`; treeEl.appendChild(row); for (const id of depNode.hotspotIds) { const h = hotspotById.get(id); if (!h) continue; const hs = document.createElement('div'); hs.className = 'tree-hotspot' + (h.id === selectedId ? ' active' : ''); - hs.innerHTML = `${escapeHtml(h.kind)}#${h.rank} ${escapeHtml(h.summary)}`; + hs.textContent = `#${h.rank} ${kindLabel(h.kind)}`; + hs.title = h.summary; hs.addEventListener('click', () => selectHotspot(h)); treeEl.appendChild(hs); } @@ -354,20 +695,23 @@ function renderTree(hotspots) { } } -async function loadSourcePreview(h) { +async function loadSourcePreview(h, pathOverride) { const preview = document.getElementById('source-preview'); const code = document.getElementById('source-code'); - const path = (h.paths || [])[0]; - if (!path) { + const pathLabel = document.getElementById('source-path'); + const path = pathOverride || primaryPath(h); + if (!path || path === '(dependency-hub)') { preview.classList.add('hidden'); return; } + pathLabel.textContent = shortPath(path); + pathLabel.title = normalizeDisplayPath(path); try { const params = new URLSearchParams({ path, line: '1' }); const res = await fetch(`/source?${params}`); if (!res.ok) { preview.classList.remove('hidden'); - code.textContent = `Source unavailable (${res.status}) for ${path}`; + code.textContent = `Source unavailable (${res.status})`; return; } const data = await res.json(); @@ -392,32 +736,65 @@ function symbolCountFromSummary(summary) { function renderDetail(h) { const el = document.getElementById('detail-body'); if (!h) { - el.innerHTML = '

Select a hotspot.

'; + el.className = 'detail-empty'; + el.innerHTML = ` +

Select a hotspot

+

Choose a view above, then click a row. This panel explains why the tool flagged it.

+ `; document.getElementById('source-preview').classList.add('hidden'); return; } - const paths = (h.paths || []) - .map((p) => `
${escapeHtml(p)}
`) - .join(''); + el.className = ''; + const help = KIND_HELP[h.kind] || {}; const rid = hotspotRepo(h); const repoLabel = rid ? repos.find((r) => r.id === rid)?.name || rid : ''; - const symCount = - h.kind === 'debt-candidate' ? symbolCountFromSummary(h.summary) : null; + const symCount = h.kind === 'debt-candidate' ? symbolCountFromSummary(h.summary) : null; + const paths = (h.paths || []).filter((p) => p && p !== '(dependency-hub)'); + const pathHtml = paths.length + ? paths + .map( + (p, i) => + `` + ) + .join('') + : '

No file paths (landscape-level hotspot).

'; + el.innerHTML = ` -

- ${escapeHtml(h.severity)} - ${escapeHtml(h.kind)} - ${escapeHtml(h.evidence_state)} - ${escapeHtml(h.producer)} - #${h.rank} - ${repoLabel ? `${escapeHtml(repoLabel)}` : ''} -

-

${escapeHtml(h.summary)}

- ${symCount ? `

Symbol count: ${escapeHtml(symCount)}

` : ''} -

id: ${escapeHtml(h.id)}

- ${paths || '

(no file paths)

'} -

producer_ref: ${escapeHtml(h.producer_ref || '')}

+
+
+ ${escapeHtml(kindLabel(h.kind))} + ${escapeHtml(h.severity in SEV_RANK ? h.severity : 'info')} + ${repoLabel ? `${escapeHtml(repoLabel)}` : ''} + rank #${safeRank(h.rank)} +
+

${escapeHtml(h.summary)}

+
+
+

Why is this here?

+

${escapeHtml(help.why || kindWhyLine(h.kind))}

+ ${help.limit ? `

${escapeHtml(help.limit)}

` : ''} + ${symCount ? `

Symbol count in file: ${escapeHtml(symCount)} (ctags).

` : ''} +

Tool: ${escapeHtml(h.producer)} · Evidence: ${escapeHtml(h.evidence_state)}

+
+
+

Paths

+ ${pathHtml} +
+
+ Raw provenance +
+

id: ${escapeHtml(h.id)}

+

producer_ref: ${escapeHtml(h.producer_ref || '')}

+
+
`; + + el.querySelectorAll('.path-chip').forEach((chip) => { + chip.addEventListener('click', () => { + loadSourcePreview(h, chip.dataset.path); + }); + }); + loadSourcePreview(h); } @@ -425,18 +802,287 @@ function selectHotspot(h) { selectedId = h?.id ?? null; renderDetail(h); render(); + requestAnimationFrame(() => { + const active = document.querySelector('.hotspot-card.active'); + active?.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); + }); } -function render() { +function switchTab(tab) { + activeTab = tab; + document.querySelectorAll('.tab-btn').forEach((btn) => { + btn.classList.toggle('active', btn.dataset.tab === tab); + }); + document.querySelectorAll('.tab-panel').forEach((panel) => { + panel.classList.toggle('active', panel.id === `tab-${tab}`); + }); + if (tab === 'overview') renderOverview(); + else if (tab === 'gaps') renderGapsTab(); + else renderFindingsTab(); +} + +function boolBadge(val, yes, no) { + if (val === true) return `${escapeHtml(yes)}`; + if (val === false) return `${escapeHtml(no)}`; + return `unknown`; +} + +function formatLanguageEntry(lang, data) { + if (data && typeof data === 'object' && data.files != null) { + const files = Number(data.files) || 0; + const pct = + data.ratio != null && !Number.isNaN(Number(data.ratio)) + ? `, ${Math.round(Number(data.ratio) * 100)}%` + : ''; + return `${lang} (${files} file${files === 1 ? '' : 's'}${pct})`; + } + if (typeof data === 'number') return `${lang} (${data})`; + return lang; +} + +function formatLanguagesSummary(languages, primaryLanguage) { + if (!languages || typeof languages !== 'object' || !Object.keys(languages).length) { + return primaryLanguage || 'unknown'; + } + return Object.entries(languages) + .sort((a, b) => { + const af = typeof a[1] === 'object' ? Number(a[1].files) || 0 : Number(a[1]) || 0; + const bf = typeof b[1] === 'object' ? Number(b[1].files) || 0 : Number(b[1]) || 0; + return bf - af; + }) + .slice(0, 4) + .map(([lang, data]) => formatLanguageEntry(lang, data)) + .join(' · '); +} + +function findingsGroupsFromHotspots(hotspots) { + const byKind = new Map(); + for (const h of hotspots) { + if (!byKind.has(h.kind)) byKind.set(h.kind, []); + byKind.get(h.kind).push(h); + } + return [...byKind.entries()] + .sort((a, b) => { + const ia = KIND_SECTION_ORDER.indexOf(a[0]); + const ib = KIND_SECTION_ORDER.indexOf(b[0]); + if (ia !== -1 && ib !== -1) return ia - ib; + if (ia !== -1) return -1; + if (ib !== -1) return 1; + return a[0].localeCompare(b[0]); + }) + .map(([kind, items]) => ({ + kind, + count: items.length, + items: [...items] + .sort((a, b) => a.rank - b.rank) + .map((h) => ({ + id: h.id, + rank: h.rank, + summary: h.summary, + severity: h.severity, + evidence_ref: `hotspot:${h.id}`, + })), + })); +} + +function renderOverview() { + const cardEl = document.getElementById('landscape-card'); + const scaleEl = document.getElementById('findings-scale'); + const repoEl = document.getElementById('repo-matrix'); + const kindEl = document.getElementById('kind-summary'); + const stepsEl = document.getElementById('next-steps'); + + const card = landscapeCard || {}; + const id = card.identity || {}; + const scale = card.scale || {}; + const activity = card.activity || {}; + const maturity = card.maturity || {}; + const health = card.health_signals || {}; + + const langs = formatLanguagesSummary(id.languages, id.primary_language); + + cardEl.innerHTML = ` +

${escapeHtml(id.name || 'Landscape')}

+
+
Language${escapeHtml(String(langs))}
+
Files${scale.total_files != null ? escapeHtml(String(scale.total_files)) : 'unknown'}
+ ${scale.total_loc != null ? `
LOC (approx)${escapeHtml(String(scale.total_loc))}
` : ''} +
Repos${repos.length || '—'}
+
Last commit${escapeHtml(activity.last_commit || 'unknown')}
+
Contributors${activity.contributors != null ? escapeHtml(String(activity.contributors)) : 'unknown'}
+
+
+ ${boolBadge(maturity.has_readme, 'README', 'no README')} + ${boolBadge(maturity.has_ci, 'CI', 'no CI')} + ${boolBadge(maturity.has_tests, 'tests', 'no tests')} + ${boolBadge(maturity.has_docker, 'Docker', 'no Docker')} +
+ ${health.staleness || health.test_coverage_hint ? `

Staleness: ${escapeHtml(health.staleness || 'unknown')} · Test hint: ${escapeHtml(health.test_coverage_hint || 'unknown')}

` : ''} + `; + + const shown = findingsShownCount(); + const total = findingsTotalCount(); + const truncated = manifest?.hotspots_truncated && !usingFullHotspots; + scaleEl.innerHTML = ` +

Scan found ${total} findings; this bundle shows ${shown}${truncated ? ' (budget truncated)' : ''}.

+ ${truncated ? '

Open the Findings tab and use Show all findings from scan to load the full list.

' : ''} + ${usingFullHotspots && shown === total ? '

Showing all findings from scan.

' : ''} + `; + + if (repos.length) { + repoEl.innerHTML = ` +

Repositories

+ + ${repos.map((r) => ``).join('')}
NamePath
${escapeHtml(r.name || r.id)}${escapeHtml(shortPath(r.path))}
+ `; + } else { + repoEl.innerHTML = ''; + } + + const kc = usingFullHotspots ? kindCounts(allHotspots) : manifest?.kind_counts || kindCounts(allHotspots); + const kcTotal = usingFullHotspots + ? kindCounts(allHotspots) + : manifest?.kind_counts_total || kindCounts(allHotspots); + kindEl.innerHTML = ` +

Findings by kind (shown / scan total)

+
    ${Object.keys({ ...kcTotal, ...kc }) + .sort((a, b) => { + const ia = KIND_SECTION_ORDER.indexOf(a); + const ib = KIND_SECTION_ORDER.indexOf(b); + if (ia !== -1 && ib !== -1) return ia - ib; + if (ia !== -1) return -1; + if (ib !== -1) return 1; + return a.localeCompare(b); + }) + .map((k) => `
  • ${escapeHtml(kindLabel(k))} ${kc[k] ?? 0} / ${kcTotal[k] ?? kc[k] ?? 0}
  • `) + .join('')}
+ `; + + const nextSection = landscapeReport?.sections?.find((s) => s.id === 'next_steps'); + const items = nextSection?.items || allHotspots.slice(0, 5).map((h) => ({ + rank: h.rank, + kind: h.kind, + summary: h.summary, + evidence_ref: `hotspot:${h.id}`, + })); + stepsEl.innerHTML = ` +

Where to look first

+
    ${items + .map( + (it) => + `
  1. ` + ) + .join('')}
+ `; + stepsEl.querySelectorAll('.next-link').forEach((btn) => { + btn.addEventListener('click', () => { + const ref = btn.dataset.ref || ''; + const hid = ref.replace(/^hotspot:/, ''); + const h = allHotspots.find((x) => x.id === hid); + if (h) { + switchTab('findings'); + selectHotspot(h); + } + }); + }); +} + +function renderGapsTab() { + const table = document.getElementById('gaps-table'); + if (!gaps.length) { + table.innerHTML = '

No gaps recorded — all configured layers produced output or explicit skip records.

'; + return; + } + table.innerHTML = ` + + ${gaps + .map( + (g) => + `` + ) + .join('')}
SurfaceStatusSummaryRecipe
${escapeHtml(g.surface || '')}${escapeHtml(g.status || '')}${escapeHtml(g.summary || '')}${escapeHtml(g.recipe || g.recipe_ref || g.producer_ref || '—')}
+ `; +} + +function renderFindingsSections() { + const el = document.getElementById('findings-sections'); + const groups = findingsGroupsFromHotspots(filteredHotspots()); + if (!groups.length) { + el.innerHTML = ''; + return; + } + el.innerHTML = ` +

Sections (map.md parity)

+
${groups + .map( + (g) => ` +
+ ${escapeHtml(kindLabel(g.kind))} ${g.count ?? (g.items?.length ?? 0)} +
    ${(g.items || []) + .slice(0, 8) + .map( + (it) => + `
  • ` + ) + .join('')}${(g.items?.length ?? 0) > 8 ? `
  • +${g.items.length - 8} more in ranked list
  • ` : ''}
+
` + ) + .join('')}
+ `; + el.querySelectorAll('.section-link').forEach((btn) => { + btn.addEventListener('click', () => { + const h = allHotspots.find((x) => x.id === btn.dataset.id); + if (h) selectHotspot(h); + }); + }); +} + +function updateLoadAllButton() { + const btn = document.getElementById('load-all-btn'); + if (!manifest?.hotspots_truncated && !usingFullHotspots) { + btn.classList.add('hidden'); + return; + } + const total = manifest?.hotspots_total ?? allHotspots.length; + btn.classList.remove('hidden'); + btn.textContent = usingFullHotspots + ? `Showing all ${allHotspots.length} findings from scan` + : `Show all ${total} findings from scan`; + btn.disabled = usingFullHotspots; +} + +async function loadFullHotspots() { + if (usingFullHotspots) return; + const full = await loadJSONL('/bundle/hotspots-full.jsonl'); + if (!full.length) return; + allHotspots = full; + usingFullHotspots = true; + updateManifestFooter(); + renderFilters(); + updateLoadAllButton(); + if (activeTab === 'overview') renderOverview(); + renderFindingsTab(); +} + +function renderFindingsTab() { const hotspots = filteredHotspots(); const hotspotById = new Map(hotspots.map((h) => [h.id, h])); if (selectedId && !hotspotById.has(selectedId)) { selectedId = null; renderDetail(null); } + renderFilterExplainer(); renderBanner(); - renderTour(hotspots, hotspotById); + renderTour(hotspots); renderTree(hotspots); + renderFindingsSections(); + updateLoadAllButton(); +} + +function render() { + if (activeTab === 'overview') renderOverview(); + else if (activeTab === 'gaps') renderGapsTab(); + else renderFindingsTab(); } async function main() { @@ -444,27 +1090,29 @@ async function main() { allHotspots = await loadJSONL('/bundle/hotspots.jsonl'); gaps = await loadJSONL('/bundle/gaps.jsonl'); repos = (await loadJSON('/bundle/repos.json')) || []; + landscapeCard = await loadJSON('/bundle/landscape-card.json'); + landscapeReport = await loadJSON('/bundle/landscape-report.json'); - document.getElementById('manifest-info').textContent = manifest - ? `target: ${manifest.target_root} · hotspots: ${manifest.hotspot_count} · gaps: ${manifest.gap_count}` - : 'no manifest'; + updateManifestFooter(); + renderKindGuide(); + renderViewBar(); renderFilters(); renderBanner(); + document.querySelectorAll('.tab-btn').forEach((btn) => { + btn.addEventListener('click', () => switchTab(btn.dataset.tab)); + }); + document.getElementById('load-all-btn').addEventListener('click', () => loadFullHotspots()); document.getElementById('search-input').addEventListener('input', (e) => { searchQuery = e.target.value; - render(); + if (activeTab === 'findings') renderFindingsTab(); }); - render(); - if (allHotspots.length) { - const first = filteredHotspots()[0] || allHotspots[0]; - selectHotspot(first); - } + switchTab('overview'); } main().catch((e) => { document.getElementById('detail-body').innerHTML = - `

Failed to load bundle. Run: npm run serve -- --bundle <orient-dir>

${escapeHtml(e.message)}
`; + `

Failed to load bundle. Run: npm run serve -- --bundle <bundle-dir>

${escapeHtml(e.message)}
`; }); diff --git a/viewer/src/index.html b/viewer/src/index.html index 5ce53823..ed93eb4d 100644 --- a/viewer/src/index.html +++ b/viewer/src/index.html @@ -3,42 +3,93 @@ - Portolan Orient + Portolan
-
-

Portolan Orient

-

Evidence-backed hotspots — not LLM architecture truth

+
+

Portolan

+

Landscape report from local scanner evidence — not an AI architecture graph

- - + +
+ -
-
-
-

Hotspot tour

-
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    -
    -

    Directory heat map

    -
    + +
    +
    +
    + +
    +
    +
    + What counts as a finding? Tools & limits +
    +
    +
    +
    +
    +
    +

    By folder

    + +
    +
    +
    +
    +
    +

    Ranked list

    + +
    +
      +
      +
      +

      Detail

      +
      +

      Select a finding

      +

      Use the list or folder tree. Detail explains which tool flagged it.

      +
      + +
      +
      +
      -
      -

      Detail

      -

      Select a hotspot.

      -
      + diff --git a/viewer/src/styles.css b/viewer/src/styles.css index bb9102f3..647f1fbd 100644 --- a/viewer/src/styles.css +++ b/viewer/src/styles.css @@ -1,29 +1,37 @@ :root { - --bg: #0f1419; - --panel: #1a2332; - --text: #e7ecf3; + --bg: #0c0f14; + --panel: #151b24; + --panel-elevated: #1c2430; + --border: #2a3548; + --text: #eef2f8; --muted: #8b9bb4; - --dup: #e85d5d; - --static: #e8a35d; - --dep: #5d9ae8; - --debt: #b85de8; - --gap: #6b7280; - --sev-critical: #ff4d4d; + --accent: #5d9ae8; + --sev-critical: #ff5c5c; --sev-high: #ff8c42; --sev-medium: #e8a35d; --sev-low: #5d9ae8; --sev-info: #8b9bb4; - --banner-warn: #3d2e14; - --banner-gap: #1e2a3d; + --kind-duplication: #e85d5d; + --kind-static: #e8a35d; + --kind-config: #4ec9b0; + --kind-debt: #b885ff; + --kind-dep: #5d9ae8; + --banner-warn: #2a2210; + --banner-gap: #141c28; + --radius: 10px; + --shadow: 0 2px 12px rgba(0, 0, 0, 0.35); } * { box-sizing: border-box; } + body { margin: 0; - font-family: system-ui, sans-serif; + font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; background: var(--bg); color: var(--text); + line-height: 1.5; } + .sr-only { position: absolute; width: 1px; @@ -34,11 +42,20 @@ body { clip: rect(0, 0, 0, 0); border: 0; } -header, footer { - padding: 1rem 1.5rem; - border-bottom: 1px solid #2a3548; + +header { + padding: 1.25rem 1.5rem 1rem; + border-bottom: 1px solid var(--border); + background: linear-gradient(180deg, #121820 0%, var(--bg) 100%); } -footer { border-top: 1px solid #2a3548; border-bottom: none; font-size: 0.85rem; color: var(--muted); } + +footer { + padding: 0.75rem 1.5rem; + border-top: 1px solid var(--border); + font-size: 0.8rem; + color: var(--muted); +} + .header-top { display: flex; flex-wrap: wrap; @@ -46,174 +63,783 @@ footer { border-top: 1px solid #2a3548; border-bottom: none; font-size: 0.85rem; justify-content: space-between; gap: 1rem; } -h1 { margin: 0; font-size: 1.25rem; } -#subtitle { margin: 0.25rem 0 0; color: var(--muted); font-size: 0.9rem; } -.search-wrap { flex: 1; min-width: 200px; max-width: 420px; } + +.brand h1 { + margin: 0; + font-size: 1.35rem; + font-weight: 650; + letter-spacing: -0.02em; +} + +#subtitle { + margin: 0.35rem 0 0; + color: var(--muted); + font-size: 0.9rem; + max-width: 36rem; +} + +.search-wrap { + flex: 1; + min-width: 220px; + max-width: 440px; +} + #search-input { width: 100%; - padding: 0.5rem 0.75rem; - border-radius: 6px; - border: 1px solid #2a3548; + padding: 0.65rem 0.9rem; + border-radius: var(--radius); + border: 1px solid var(--border); background: var(--panel); color: var(--text); font-size: 0.95rem; + transition: border-color 0.15s, box-shadow 0.15s; +} + +#search-input:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 3px rgba(93, 154, 232, 0.2); +} + +.intro-line { + margin: 0.85rem 0 0; + font-size: 0.88rem; + color: var(--muted); + max-width: 52rem; + line-height: 1.45; +} + +.intro-line strong { color: #c5d4e8; } + +.view-section { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.5rem; + margin-top: 0.85rem; +} + +.view-bar { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; +} + +.view-btn { + font-size: 0.8rem; + padding: 0.4rem 0.75rem; + border-radius: var(--radius); + border: 1px solid var(--border); + background: var(--panel); + color: var(--text); + cursor: pointer; + transition: background 0.15s, border-color 0.15s; } + +.view-btn:hover { + background: var(--panel-elevated); + border-color: #3d4d66; +} + +.view-btn.active { + background: #1e3a5f; + border-color: var(--accent); + color: #fff; +} + +.view-btn.custom-tag { + cursor: default; + font-style: italic; +} + +.filter-explainer { + margin-top: 0.65rem; + padding: 0.65rem 0.85rem; + border-radius: var(--radius); + background: var(--panel); + border: 1px solid var(--border); + font-size: 0.82rem; + line-height: 1.45; +} + +.explainer-main { margin: 0 0 0.35rem; color: var(--text); } +.explainer-meta { margin: 0; color: var(--muted); font-size: 0.78rem; } + +.kind-guide { + margin-top: 0.65rem; + font-size: 0.82rem; + color: var(--muted); +} + +.kind-guide summary { + cursor: pointer; + color: var(--accent); + user-select: none; +} + +.kind-guide-body { + margin-top: 0.65rem; + padding: 0.5rem 0; +} + +.guide-item { + margin-bottom: 0.85rem; + padding-bottom: 0.65rem; + border-bottom: 1px solid var(--border); +} + +.guide-item:last-child { border-bottom: none; margin-bottom: 0; } + +.guide-item h4 { margin: 0 0 0.35rem; font-size: 0.85rem; } + +.guide-item p { margin: 0.25rem 0; } + +.guide-meta { + font-size: 0.75rem; + color: var(--muted); +} + .status-banner { margin-top: 0.75rem; - padding: 0.5rem 0.75rem; - border-radius: 6px; + padding: 0.6rem 0.85rem; + border-radius: var(--radius); font-size: 0.85rem; + line-height: 1.4; } + .status-banner.hidden { display: none; } -.status-banner.truncation { background: var(--banner-warn); color: #f5d9a8; } -.status-banner.gaps { background: var(--banner-gap); color: var(--muted); } +.status-banner.truncation { + background: var(--banner-warn); + color: #f0d9a8; + border: 1px solid #4a3a18; +} +.status-banner.gaps { + background: var(--banner-gap); + color: var(--muted); + border: 1px solid var(--border); +} + .filter-bar { display: flex; flex-wrap: wrap; - gap: 0.35rem; + gap: 0.5rem; margin-top: 0.75rem; + padding-top: 0.75rem; + border-top: 1px solid var(--border); } + .filter-group { display: flex; flex-wrap: wrap; align-items: center; - gap: 0.25rem; - margin-right: 0.75rem; + gap: 0.3rem; } + .filter-label { - font-size: 0.7rem; + font-size: 0.68rem; text-transform: uppercase; + letter-spacing: 0.04em; color: var(--muted); - margin-right: 0.25rem; + margin-right: 0.15rem; } + .chip { font-size: 0.75rem; - padding: 0.2rem 0.5rem; + padding: 0.25rem 0.55rem; border-radius: 999px; - border: 1px solid #2a3548; + border: 1px solid var(--border); background: transparent; color: var(--muted); cursor: pointer; + transition: all 0.12s; } -.chip:hover { border-color: var(--muted); color: var(--text); } + +.chip:hover { + border-color: #4a5568; + color: var(--text); +} + .chip.active { - background: #2a3548; + background: #243044; color: var(--text); - border-color: #4a5568; + border-color: var(--accent); } + +.chip.locked { + background: #1a2838; + color: var(--text); + border-color: #4a6a8a; + box-shadow: inset 0 0 0 1px rgba(93, 154, 232, 0.25); +} + +.chip.locked::after { + content: ' · view'; + font-size: 0.62rem; + opacity: 0.65; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +.chip.inactive { + opacity: 0.55; +} + +.chip .chip-count { + opacity: 0.7; + margin-left: 0.15rem; +} + .layout { display: grid; - grid-template-columns: minmax(220px, 1fr) minmax(280px, 1.2fr) minmax(260px, 1fr); gap: 1rem; - padding: 1rem; + padding: 1rem 1.5rem 1.5rem; + max-width: 1600px; + margin: 0 auto; +} + +.layout-three { + grid-template-columns: minmax(200px, 0.85fr) minmax(260px, 1fr) minmax(300px, 1.15fr); +} + +@media (max-width: 1100px) { + .layout-three { grid-template-columns: 1fr 1fr; } + .layout-three .panel-detail { grid-column: 1 / -1; } } -@media (max-width: 960px) { - .layout { grid-template-columns: 1fr; } + +@media (max-width: 700px) { + .layout-three { grid-template-columns: 1fr; } + .layout-three .panel-detail { grid-column: auto; } } + .panel { background: var(--panel); - border-radius: 8px; - padding: 1rem; - min-height: 200px; - max-height: 70vh; - overflow: auto; + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: var(--shadow); + min-height: 280px; + max-height: calc(100vh - 220px); + overflow: hidden; + display: flex; + flex-direction: column; +} + +.panel-list, +.panel-tree { padding: 0; } +.panel-detail { padding: 1.25rem; overflow: auto; } + +.panel-hint { + margin: 0; + padding: 0 1rem 0.65rem; + font-size: 0.75rem; + color: var(--muted); + line-height: 1.4; + border-bottom: 1px solid var(--border); + flex-shrink: 0; +} + +.panel-head { + display: flex; + align-items: baseline; + gap: 0.5rem; + padding: 1rem 1rem 0.5rem; + border-bottom: 1px solid var(--border); + flex-shrink: 0; +} + +.panel-head h2 { + margin: 0; + font-size: 0.95rem; + font-weight: 600; } -.panel h2 { margin: 0 0 0.75rem; font-size: 1rem; } + .count-badge { font-size: 0.75rem; color: var(--muted); - font-weight: normal; } -.badge { - display: inline-block; + +#hotspot-list { + list-style: none; + margin: 0; + padding: 0.5rem; + overflow-y: auto; + flex: 1; +} + +.hotspot-card { + display: block; + width: 100%; + text-align: left; + margin-bottom: 0.4rem; + padding: 0.75rem 0.85rem; + border-radius: 8px; + border: 1px solid transparent; + background: var(--panel-elevated); + color: inherit; + cursor: pointer; + transition: border-color 0.12s, background 0.12s; + font: inherit; +} + +.hotspot-card:hover { + border-color: #3d4d66; + background: #1f2836; +} + +.hotspot-card.active { + border-color: var(--accent); + background: #1a2a40; + box-shadow: inset 3px 0 0 var(--accent); +} + +.card-top { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.35rem; +} + +.card-rank { font-size: 0.7rem; - padding: 0.1rem 0.35rem; - border-radius: 4px; - background: #2a3548; color: var(--muted); - margin-right: 0.25rem; + font-variant-numeric: tabular-nums; + min-width: 1.5rem; } -.badge.sev-critical { background: color-mix(in srgb, var(--sev-critical) 40%, #2a3548); color: #ffc9c9; } -.badge.sev-high { background: color-mix(in srgb, var(--sev-high) 40%, #2a3548); color: #ffd4b8; } -.badge.sev-medium { background: color-mix(in srgb, var(--sev-medium) 40%, #2a3548); color: #ffe4c4; } -.badge.sev-low { background: color-mix(in srgb, var(--sev-low) 40%, #2a3548); color: #c9e0ff; } -.badge.sev-info { background: #2a3548; color: var(--muted); } -#hotspot-list { padding-left: 1.2rem; margin: 0; list-style: decimal; } -#hotspot-list li { - margin-bottom: 0.5rem; - cursor: pointer; - padding: 0.25rem 0; + +.kind-pill { + font-size: 0.68rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.03em; + padding: 0.12rem 0.4rem; border-radius: 4px; } -#hotspot-list li:hover { color: #fff; } -#hotspot-list li.active { background: #2a3548; padding-left: 0.25rem; } -.heat-tree { font-size: 0.85rem; } + +.kind-pill.duplication { background: rgba(232, 93, 93, 0.2); color: #ffb4b4; } +.kind-pill.static-finding { background: rgba(232, 163, 93, 0.2); color: #ffd4a8; } +.kind-pill.config { background: rgba(78, 201, 176, 0.2); color: #9ee8d8; } +.kind-pill.debt-candidate { background: rgba(184, 133, 255, 0.2); color: #d4b8ff; } +.kind-pill.dep-hub { background: rgba(93, 154, 232, 0.2); color: #b8d4ff; } + +.card-summary { + font-size: 0.9rem; + line-height: 1.35; + margin: 0; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.card-why { + margin: 0.35rem 0 0; + font-size: 0.78rem; + color: var(--muted); + line-height: 1.35; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.why-section { + background: var(--panel-elevated); + border: 1px solid var(--border); + border-radius: 8px; + padding: 0.85rem 1rem; +} + +.card-path { + margin-top: 0.35rem; + font-family: ui-monospace, 'Cascadia Code', monospace; + font-size: 0.72rem; + color: var(--muted); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.heat-tree { + font-size: 0.8rem; + padding: 0.5rem 0.65rem 0.75rem; + overflow-y: auto; + flex: 1; + min-height: 0; +} + .tree-node { margin-left: 0; } + .tree-row { display: flex; align-items: center; gap: 0.35rem; - padding: 0.2rem 0.25rem; + padding: 0.15rem 0.25rem; border-radius: 4px; - cursor: pointer; } -.tree-row:hover { background: #243044; } -.tree-row.active { background: #2d3f5c; } + +.tree-row:hover { background: #1f2836; } + .tree-toggle { width: 1rem; text-align: center; color: var(--muted); flex-shrink: 0; user-select: none; + cursor: pointer; } + .tree-toggle.empty { visibility: hidden; } -.tree-name { flex: 1; font-family: monospace; font-size: 0.8rem; } + +.tree-name { + flex: 1; + font-family: ui-monospace, monospace; + font-size: 0.75rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + .tree-meta { - font-size: 0.7rem; + font-size: 0.68rem; color: var(--muted); flex-shrink: 0; } -.tree-children { margin-left: 1rem; border-left: 1px solid #2a3548; } + +.tree-children { + margin-left: 0.75rem; + border-left: 1px solid var(--border); +} + .tree-children.collapsed { display: none; } + .tree-hotspot { - margin-left: 1.25rem; - padding: 0.2rem 0.35rem; + margin-left: 1rem; + padding: 0.15rem 0.35rem; cursor: pointer; border-radius: 4px; - font-size: 0.8rem; + font-size: 0.72rem; + color: var(--muted); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } -.tree-hotspot:hover { background: #243044; } -.tree-hotspot.active { background: #2d3f5c; } + +.tree-hotspot:hover { background: #1f2836; color: var(--text); } +.tree-hotspot.active { background: #1a2a40; color: var(--text); } + .sev-bar { - width: 4px; - height: 1rem; + width: 3px; + height: 0.9rem; border-radius: 2px; flex-shrink: 0; } -.sev-bar.critical { background: var(--sev-critical); } -.sev-bar.high { background: var(--sev-high); } -.sev-bar.medium { background: var(--sev-medium); } -.sev-bar.low { background: var(--sev-low); } -.sev-bar.info { background: var(--sev-info); } -.path { font-family: monospace; font-size: 0.8rem; color: var(--muted); word-break: break-all; } -.source-preview { margin-top: 1rem; border-top: 1px solid #2a3548; padding-top: 0.75rem; } -.source-preview.hidden { display: none; } -.source-preview h3 { margin: 0 0 0.5rem; font-size: 0.9rem; color: var(--muted); } -#source-code { + +.sev-bar.sev-critical { background: var(--sev-critical); } +.sev-bar.sev-high { background: var(--sev-high); } +.sev-bar.sev-medium { background: var(--sev-medium); } +.sev-bar.sev-low { background: var(--sev-low); } +.sev-bar.sev-info { background: var(--sev-info); } + +.detail-empty { + color: var(--muted); + padding: 2rem 0.5rem; + text-align: center; +} + +.empty-title { + margin: 0 0 0.5rem; + font-size: 1.1rem; + color: var(--text); +} + +.empty-hint { margin: 0; - padding: 0.75rem; - background: #0a0e14; + font-size: 0.9rem; + max-width: 28rem; + margin-inline: auto; +} + +.detail-header { + margin-bottom: 1rem; +} + +.detail-title { + margin: 0 0 0.75rem; + font-size: 1.15rem; + font-weight: 600; + line-height: 1.35; +} + +.detail-meta { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; +} + +.badge { + display: inline-block; + font-size: 0.72rem; + padding: 0.2rem 0.45rem; border-radius: 6px; + background: #243044; + color: var(--muted); +} + +.badge.sev-critical { background: rgba(255, 92, 92, 0.25); color: #ffc9c9; } +.badge.sev-high { background: rgba(255, 140, 66, 0.25); color: #ffd4b8; } +.badge.sev-medium { background: rgba(232, 163, 93, 0.25); color: #ffe4c4; } +.badge.sev-low { background: rgba(93, 154, 232, 0.25); color: #c9e0ff; } +.badge.sev-info { background: #243044; color: var(--muted); } + +.detail-section { + margin-top: 1.25rem; +} + +.detail-section h3 { + margin: 0 0 0.5rem; font-size: 0.75rem; - line-height: 1.45; - overflow-x: auto; - max-height: 320px; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--muted); + font-weight: 600; +} + +.path-chip { + display: block; + font-family: ui-monospace, monospace; + font-size: 0.78rem; + padding: 0.45rem 0.6rem; + margin-bottom: 0.35rem; + border-radius: 6px; + background: #0f1419; + border: 1px solid var(--border); + color: #c5d0e0; + word-break: break-all; + cursor: pointer; +} + +.path-chip:hover { + border-color: var(--accent); + color: #fff; +} + +.path-chip.primary { border-color: #3d5a80; } + +.evidence-toggle { + font-size: 0.8rem; + color: var(--muted); + cursor: pointer; + margin-top: 1rem; +} + +.evidence-toggle:hover { color: var(--text); } + +.evidence-body { + margin-top: 0.5rem; + font-size: 0.75rem; + color: var(--muted); + word-break: break-all; +} + +.evidence-body.hidden { display: none; } + +.source-preview { + margin-top: 1.5rem; + border-top: 1px solid var(--border); + padding-top: 1rem; +} + +.source-preview.hidden { display: none; } + +.source-head { + display: flex; + flex-wrap: wrap; + align-items: baseline; + justify-content: space-between; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.source-head h3 { + margin: 0; + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--muted); +} + +.source-path { + font-family: ui-monospace, monospace; + font-size: 0.72rem; + color: var(--muted); } + +#source-code { + margin: 0; + padding: 0.85rem; + background: #080b10; + border: 1px solid var(--border); + border-radius: 8px; + font-size: 0.78rem; + line-height: 1.5; + overflow: auto; + max-height: 360px; +} + #source-code .line { display: block; } -#source-code .line.highlight { background: #2d3f5c; } +#source-code .line.highlight { background: #1a2a40; } #source-code .lineno { display: inline-block; - width: 3.5rem; - color: var(--muted); + width: 3rem; + color: #5a6a82; user-select: none; + text-align: right; + padding-right: 0.75rem; +} + +.path { font-family: ui-monospace, monospace; font-size: 0.8rem; color: var(--muted); word-break: break-all; } + +.tab-bar { + display: flex; + gap: 0.35rem; + margin-top: 1rem; +} + +.tab-btn { + font-size: 0.88rem; + padding: 0.5rem 1rem; + border-radius: var(--radius); + border: 1px solid var(--border); + background: var(--panel); + color: var(--text); + cursor: pointer; +} + +.tab-btn.active { + background: #1e3a5f; + border-color: var(--accent); +} + +.tab-panel { display: none; } +.tab-panel.active { display: block; } + +main { padding: 1rem 1.5rem 2rem; } + +.report-overview { + display: flex; + flex-direction: column; + gap: 1.25rem; + max-width: 56rem; +} + +.landscape-card { + background: var(--panel); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1.25rem; + box-shadow: var(--shadow); +} + +.landscape-card h2 { margin: 0 0 0.75rem; font-size: 1.15rem; } + +.card-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); + gap: 0.75rem; +} + +.card-stat .label { + display: block; + font-size: 0.75rem; + color: var(--muted); + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.card-stat .value { font-size: 1rem; font-weight: 600; } + +.maturity-row { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.85rem; } + +.badge.ok { background: #1a3328; color: #6ee7b7; } +.badge.warn { background: #2a2210; color: #e8a35d; } +.badge.unknown { background: var(--panel-elevated); color: var(--muted); } + +.health-hint { font-size: 0.85rem; color: var(--muted); margin: 0.75rem 0 0; } + +.findings-scale .scale-line { margin: 0; } +.findings-scale .scale-hint { margin: 0.35rem 0 0; font-size: 0.88rem; color: var(--muted); } + +.repo-table, .gaps-table { + width: 100%; + border-collapse: collapse; + font-size: 0.88rem; +} + +.repo-table th, .gaps-table th, +.repo-table td, .gaps-table td { + text-align: left; + padding: 0.45rem 0.65rem; + border-bottom: 1px solid var(--border); +} + +.mono { font-family: ui-monospace, monospace; font-size: 0.82rem; } + +.kind-list { list-style: none; padding: 0; margin: 0; display: flex; flex-wrap: wrap; gap: 0.5rem; } +.kind-list li { display: flex; align-items: center; gap: 0.35rem; } + +.next-list { margin: 0; padding-left: 1.25rem; } +.next-link { + background: none; + border: none; + color: var(--accent); + cursor: pointer; + text-align: left; + padding: 0.25rem 0; + font: inherit; +} + +.gaps-panel h2 { margin: 0 0 0.35rem; } +.panel-hint { color: var(--muted); font-size: 0.88rem; margin: 0 0 1rem; } + +.findings-toolbar { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + margin-bottom: 0.75rem; +} + +.load-all-btn { + font-size: 0.82rem; + padding: 0.4rem 0.75rem; + border-radius: var(--radius); + border: 1px solid var(--accent); + background: #1a2a40; + color: var(--text); + cursor: pointer; +} + +.load-all-btn:disabled { opacity: 0.6; cursor: default; } + +.findings-sections { margin-top: 1.5rem; } +.section-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 0.75rem; } +.section-block { + background: var(--panel); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 0.5rem 0.75rem; +} + +.section-block summary { cursor: pointer; font-weight: 600; } +.section-block ul { margin: 0.5rem 0 0; padding-left: 1.1rem; font-size: 0.85rem; } +.section-link { + background: none; + border: none; + color: var(--text); + cursor: pointer; + text-align: left; + padding: 0.15rem 0; + font: inherit; } +.section-link:hover { color: var(--accent); } +.section-block .more { color: var(--muted); list-style: none; margin-left: -1.1rem; }