@fcon-tech/portolan 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +110 -0
  3. package/adapters/README.md +226 -0
  4. package/adapters/omp/portolan-mcp +19 -0
  5. package/adapters/opencode/expedition-launcher +70 -0
  6. package/adapters/opencode/install.test.ts +105 -0
  7. package/adapters/opencode/install.ts +357 -0
  8. package/adapters/pi/portolan-mcp +19 -0
  9. package/adapters/scheduling/night-watch.cron +23 -0
  10. package/core/schema/chart.schema.json +154 -0
  11. package/core/src/bin/portolan.ts +84 -0
  12. package/core/src/chart-io.rollback-fixture.ts +55 -0
  13. package/core/src/chart-io.ts +121 -0
  14. package/core/src/chart-store.ts +137 -0
  15. package/core/src/chartroom/cli.ts +63 -0
  16. package/core/src/chartroom/render.ts +213 -0
  17. package/core/src/chartroom/review-template.html +232 -0
  18. package/core/src/chartroom/review.ts +109 -0
  19. package/core/src/chartroom/template.html +1090 -0
  20. package/core/src/fan-in.ts +84 -0
  21. package/core/src/harbor/chat-format.ts +154 -0
  22. package/core/src/harbor/cli.ts +178 -0
  23. package/core/src/harbor/errors.ts +22 -0
  24. package/core/src/harbor/fingerprint.ts +29 -0
  25. package/core/src/harbor/history.ts +178 -0
  26. package/core/src/harbor/launcher.ts +155 -0
  27. package/core/src/harbor/night-policy.ts +64 -0
  28. package/core/src/harbor/proposals.ts +324 -0
  29. package/core/src/harbor/run.ts +72 -0
  30. package/core/src/harbor/settings.ts +108 -0
  31. package/core/src/harbor/snapshot.ts +187 -0
  32. package/core/src/harbor/watch.ts +103 -0
  33. package/core/src/index.ts +28 -0
  34. package/core/src/notices.ts +117 -0
  35. package/core/src/perimeter.ts +44 -0
  36. package/core/src/server/adapter-boundary.ts +66 -0
  37. package/core/src/server/main.ts +27 -0
  38. package/core/src/server/registry.ts +609 -0
  39. package/core/src/server/server.ts +123 -0
  40. package/core/src/server/test-harness.ts +161 -0
  41. package/core/src/sheets.ts +151 -0
  42. package/core/src/staleness.ts +203 -0
  43. package/core/src/tools/log.ts +215 -0
  44. package/core/src/tools/manifests.ts +912 -0
  45. package/core/src/tools/neighborhood.ts +423 -0
  46. package/core/src/tools/shared.ts +72 -0
  47. package/core/src/tools/sound.ts +634 -0
  48. package/core/src/tools/sweep.ts +198 -0
  49. package/core/src/tools/symbols.ts +176 -0
  50. package/core/src/tools/trust-report.ts +193 -0
  51. package/core/src/types.ts +162 -0
  52. package/core/src/validate.ts +106 -0
  53. package/package.json +34 -0
  54. package/skill/SKILL.md +279 -0
  55. package/skill/examples/sailing-directions-example.md +35 -0
  56. package/skill/sailing-directions.template.md +59 -0
  57. package/skill/verify/checks.ts +476 -0
  58. package/skill/verify/dry-run.ts +738 -0
  59. package/skill/verify/fixture.ts +128 -0
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Chart ontology — the vocabulary of the Padrón.
3
+ *
4
+ * Terminology is locked by docs/MANIFEST.md: vessel, fairway, port of entry,
5
+ * beacon, light, danger, anchor, trust label, pending correction, Notices to
6
+ * Mariners. Every entry carries at least one anchor and exactly one trust
7
+ * label; the store rejects writes that omit either.
8
+ */
9
+
10
+ /** The closed trust vocabulary (chart notation). */
11
+ export const TRUST_LABELS = [
12
+ "measured",
13
+ "charted",
14
+ "reported",
15
+ "doubtful",
16
+ "unsurveyed",
17
+ ] as const;
18
+
19
+ export type TrustLabel = (typeof TRUST_LABELS)[number];
20
+
21
+ /** The six chart entry kinds. */
22
+ export const ENTRY_KINDS = [
23
+ "vessel",
24
+ "fairway",
25
+ "portOfEntry",
26
+ "beacon",
27
+ "light",
28
+ "danger",
29
+ ] as const;
30
+
31
+ export type EntryKind = (typeof ENTRY_KINDS)[number];
32
+
33
+ /**
34
+ * An anchor ties a claim to evidence: a file path (with optional line), a
35
+ * manifest key, or a receipt id from the ship's log.
36
+ */
37
+ export type Anchor =
38
+ | { type: "file"; path: string; line?: number }
39
+ | { type: "manifest"; path: string; key: string }
40
+ | { type: "receipt"; id: string };
41
+
42
+ /** Render an anchor as a compact, human-readable string. */
43
+ export function formatAnchor(anchor: Anchor): string {
44
+ switch (anchor.type) {
45
+ case "file":
46
+ return anchor.line === undefined ? anchor.path : `${anchor.path}:${anchor.line}`;
47
+ case "manifest":
48
+ return `${anchor.path}#${anchor.key}`;
49
+ case "receipt":
50
+ return `receipt:${anchor.id}`;
51
+ }
52
+ }
53
+
54
+ interface EntryBase {
55
+ /** Stable identifier, unique across the chart. */
56
+ id: string;
57
+ /** At least one anchor is mandatory. */
58
+ anchors: Anchor[];
59
+ /** Exactly one trust label is mandatory. */
60
+ trust: TrustLabel;
61
+ /** Free-form qualification; never a substitute for evidence. */
62
+ note?: string;
63
+ }
64
+
65
+ /** A deployable unit. */
66
+ export interface VesselEntry extends EntryBase {
67
+ kind: "vessel";
68
+ name: string;
69
+ /**
70
+ * What the vessel does at runtime. Absent behavior is rendered as
71
+ * `unsurveyed` on the sheet — absence stays visible, never omitted.
72
+ */
73
+ behavior?: string;
74
+ /** Source paths (relative to the target root) covered by the tree signature. */
75
+ paths: string[];
76
+ }
77
+
78
+ /**
79
+ * The closed relation vocabulary on a fairway — the senses the anchors can
80
+ * actually support. Optional: a fairway without a relation stays valid and
81
+ * reads as untyped.
82
+ */
83
+ export const FAIRWAY_RELATIONS = ["build", "runtime", "config"] as const;
84
+
85
+ export type FairwayRelation = (typeof FAIRWAY_RELATIONS)[number];
86
+
87
+ /** A typed dependency edge between two vessels. */
88
+ export interface FairwayEntry extends EntryBase {
89
+ kind: "fairway";
90
+ from: string;
91
+ to: string;
92
+ /** When known: what kind of dependence the edge is. */
93
+ relation?: FairwayRelation;
94
+ }
95
+
96
+ /** An entry point into a vessel (http endpoint, cli, event, job, ...). */
97
+ export interface PortOfEntryEntry extends EntryBase {
98
+ kind: "portOfEntry";
99
+ vessel: string;
100
+ /** Short protocol family, e.g. "http", "cli", "gradle task". */
101
+ protocol: string;
102
+ }
103
+
104
+ /** A configuration surface: env var, flag, or port. */
105
+ export interface BeaconEntry extends EntryBase {
106
+ kind: "beacon";
107
+ vessel: string;
108
+ surface: "env" | "flag" | "port";
109
+ /** The configured key, e.g. "PORT", "--verbose", "8080". */
110
+ key: string;
111
+ }
112
+
113
+ /** An API contract surface: endpoint, exported symbol, CLI flag, event. */
114
+ export interface LightEntry extends EntryBase {
115
+ kind: "light";
116
+ vessel: string;
117
+ /** The contract's name, e.g. "GET /api/users" or "export function parse()". */
118
+ name: string;
119
+ }
120
+
121
+ /** A smell or risk. Categories per the locked glossary: rock / shallow / wreck. */
122
+ export interface DangerEntry extends EntryBase {
123
+ kind: "danger";
124
+ vessel: string;
125
+ category: "rock" | "shallow" | "wreck";
126
+ /** What the danger is. */
127
+ note: string;
128
+ }
129
+
130
+ export type ChartEntry =
131
+ | VesselEntry
132
+ | FairwayEntry
133
+ | PortOfEntryEntry
134
+ | BeaconEntry
135
+ | LightEntry
136
+ | DangerEntry;
137
+
138
+ /** Cheap tree signature over a vessel's paths (see design.md, decision 3). */
139
+ export interface VesselSignature {
140
+ hash: string;
141
+ files: number;
142
+ }
143
+
144
+ /**
145
+ * An entry as stored in `index.jsonl`: the chart entry plus store metadata
146
+ * (`stale` marks pending correction; `signature` is present on vessels only).
147
+ */
148
+ export type IndexedEntry = ChartEntry & {
149
+ stale: boolean;
150
+ signature?: VesselSignature;
151
+ };
152
+
153
+ /** What a Notice to Mariners reports about one entry. */
154
+ export type NoticeAction = "added" | "corrected" | "markedStale" | "retired";
155
+
156
+ export interface Notice {
157
+ action: NoticeAction;
158
+ kind: EntryKind;
159
+ id: string;
160
+ note?: string;
161
+ anchors: Anchor[];
162
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Chart entry validation — ajv (draft 2020-12) against
3
+ * core/schema/chart.schema.json, with entry-locating errors: every problem
4
+ * names the offending entry's kind and id.
5
+ */
6
+ import Ajv2020 from "ajv/dist/2020";
7
+ import schema from "../schema/chart.schema.json";
8
+ import { ENTRY_KINDS, type ChartEntry, type EntryKind } from "./types";
9
+
10
+ interface AjvErrorLike {
11
+ message?: string;
12
+ instancePath?: string;
13
+ params?: { allowedValues?: unknown[] };
14
+ }
15
+
16
+ type SubschemaValidator = ((data: unknown) => boolean) & {
17
+ errors?: AjvErrorLike[] | null;
18
+ };
19
+
20
+ const SCHEMA_ID = schema.$id;
21
+
22
+ const ajv = new Ajv2020({ allErrors: true });
23
+ ajv.addSchema(schema);
24
+
25
+ const validators = new Map<string, SubschemaValidator>();
26
+
27
+ function validatorFor(kind: EntryKind): SubschemaValidator {
28
+ let v = validators.get(kind);
29
+ if (!v) {
30
+ const got = ajv.getSchema(`${SCHEMA_ID}#/$defs/${kind}`);
31
+ if (!got) throw new Error(`chart schema has no $defs/${kind}`);
32
+ v = got as SubschemaValidator;
33
+ validators.set(kind, v);
34
+ }
35
+ return v;
36
+ }
37
+
38
+ /** One offending entry and its problems. */
39
+ export interface EntryProblem {
40
+ kind: string;
41
+ id: string;
42
+ problems: string[];
43
+ }
44
+
45
+ /** Thrown when a write batch contains invalid entries. Nothing is persisted. */
46
+ export class ChartValidationError extends Error {
47
+ readonly problems: EntryProblem[];
48
+
49
+ constructor(problems: EntryProblem[]) {
50
+ const lines = problems.map(
51
+ (p) => ` - ${p.kind}/${p.id}: ${p.problems.join("; ")}`
52
+ );
53
+ super(`chart validation failed:\n${lines.join("\n")}`);
54
+ this.name = "ChartValidationError";
55
+ this.problems = problems;
56
+ }
57
+ }
58
+
59
+ function locate(entry: unknown, index: number): { kind: string; id: string } {
60
+ const obj = (entry ?? {}) as Record<string, unknown>;
61
+ const kind = typeof obj.kind === "string" ? obj.kind : "unknown";
62
+ const id =
63
+ typeof obj.id === "string" && obj.id.length > 0 ? obj.id : `#${index} (no id)`;
64
+ return { kind, id };
65
+ }
66
+
67
+ function formatAjvErrors(errors: AjvErrorLike[]): string[] {
68
+ return errors.map((e) => {
69
+ const at = e.instancePath || "/";
70
+ const allowed = e.params?.allowedValues;
71
+ const list = Array.isArray(allowed) ? `: ${allowed.join(", ")}` : "";
72
+ return `${e.message ?? "is invalid"}${list} (at ${at})`;
73
+ });
74
+ }
75
+
76
+ /** Validate a single entry; returns its problem report, or null when valid. */
77
+ export function validateEntry(entry: unknown, index = 0): EntryProblem | null {
78
+ const { kind, id } = locate(entry, index);
79
+ if (!ENTRY_KINDS.includes(kind as EntryKind)) {
80
+ return {
81
+ kind,
82
+ id,
83
+ problems: [
84
+ `unknown kind (expected one of: ${ENTRY_KINDS.join(", ")})`,
85
+ ],
86
+ };
87
+ }
88
+ const validate = validatorFor(kind as EntryKind);
89
+ if (validate(entry)) return null;
90
+ return { kind, id, problems: formatAjvErrors(validate.errors ?? []) };
91
+ }
92
+
93
+ /**
94
+ * Validate a whole write batch. Returns the batch unchanged when every entry
95
+ * conforms; throws {@link ChartValidationError} naming every offending entry
96
+ * otherwise. Callers must treat a throw as "nothing was written".
97
+ */
98
+ export function validateEntries(entries: unknown[]): ChartEntry[] {
99
+ const problems: EntryProblem[] = [];
100
+ entries.forEach((entry, index) => {
101
+ const problem = validateEntry(entry, index);
102
+ if (problem) problems.push(problem);
103
+ });
104
+ if (problems.length > 0) throw new ChartValidationError(problems);
105
+ return entries as ChartEntry[];
106
+ }
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@fcon-tech/portolan",
3
+ "version": "0.4.5",
4
+ "description": "Portolan: an MCP server that charts codebases - Chart store, Harbor expeditions, Chart Room. Runs on Bun; requires ripgrep and ctags on PATH (wrapped, not bundled).",
5
+ "type": "module",
6
+ "mcpName": "io.github.fcon-tech/portolan",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/fcon-tech/portolan.git"
10
+ },
11
+ "engines": {
12
+ "bun": ">=1.1.0"
13
+ },
14
+ "bin": {
15
+ "portolan": "core/src/bin/portolan.ts"
16
+ },
17
+ "files": [
18
+ "core/src",
19
+ "core/schema",
20
+ "skill",
21
+ "adapters",
22
+ "!core/src/**/*.test.ts"
23
+ ],
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "dependencies": {
28
+ "@modelcontextprotocol/sdk": "^1.30.0"
29
+ },
30
+ "devDependencies": {
31
+ "ajv": "^8.20.0"
32
+ },
33
+ "license": "MIT"
34
+ }
package/skill/SKILL.md ADDED
@@ -0,0 +1,279 @@
1
+ ---
2
+ name: portolan-expedition
3
+ description: Survey a target with Portolan — the Cartographer's method for an Expedition. One approval, five survey passes onto the Chart, soundings as you chart, honest unsurveyed waters, Sailing Directions for the Governor.
4
+ ---
5
+
6
+ # Portolan — the Cartographer's method
7
+
8
+ You are the Cartographer. The Governor gives one phrase: "survey \<target\>
9
+ with Portolan". From that phrase to the delivered Sailing
10
+ Directions, everything is yours to do. This document is the method; follow it
11
+ in order.
12
+
13
+ Vocabulary is locked by docs/MANIFEST.md: Governor, Cartographer, Expedition,
14
+ Chart, vessel, fairway, port of entry, beacon, light, danger, unsurveyed,
15
+ pending correction, Notices to Mariners, Sailing Directions. Trust labels:
16
+ `measured`, `charted`, `reported`, `doubtful`, `unsurveyed`. Use these words
17
+ for these things and no others.
18
+
19
+ ## 0. The harbor watch: proposals at session start
20
+
21
+ When a session enters a province with a standing Chart (the machine index
22
+ `<target>/.portolan/chart/index.jsonl` exists), the harbor watch runs
23
+ before other work:
24
+
25
+ 1. Call `expeditions.propose` (no input). The queue is computed, never imagined:
26
+ vessels marked `pending correction`, charted vessels with no recorded
27
+ behavior or no charted light, and landscape present since the last
28
+ survey snapshot. Propose nothing the queue does not contain.
29
+ 2. Empty queue: say nothing about proposals and proceed with the
30
+ Governor's ask.
31
+ 3. Otherwise, present the top proposals in one chat message before other
32
+ work — each with its kind (repair, gap, new-land), its evidence
33
+ summary, and its scope. Ask for a one-phrase decision.
34
+ 4. Record the decision with `expeditions.decide`: the proposal's
35
+ fingerprint plus accepted or declined. A refusal holds while the evidence is unchanged; do not
36
+ re-ask the same proposal.
37
+ 5. An accepted proposal is the next Expedition's starting scope: begin
38
+ from its evidence (the drifted vessels, the gapped vessel, or the new
39
+ land) and survey per section 4. A province with no standing Chart has
40
+ no queue; that is a first survey, so start at section 1.
41
+ 6. Before any edit: a task touching more than one file or vessel requires
42
+ calling `chart.neighborhood` for each touched vessel first — sound the
43
+ neighborhood before the first edit.
44
+
45
+ ## 1. Lift-off
46
+
47
+ When the Governor asks, in one phrase, to survey a target with Portolan:
48
+
49
+ 1. Say that the Expedition has started. Show no command text; the Governor is
50
+ never handed any.
51
+ 2. Ask the one approval (section 2), before any network access and before any
52
+ installation.
53
+ 3. Install Portolan into your harness yourself: the Portolan MCP server (one
54
+ server, stdio, bound to the target root) and this skill, through the
55
+ adapter for your harness (opencode first; pi and omp use the launch
56
+ shims). If the server or its adapter is missing, stop and report the
57
+ blocker: improvise no substitute server and hand the Governor nothing to
58
+ run.
59
+ 4. Receipt every executed command in the ship's log with `log.append`.
60
+ 5. Proceed to the survey (section 4). Take no further Governor action for any
61
+ step: install, survey, and brief are all yours.
62
+
63
+ ## 2. The one approval
64
+
65
+ Ask exactly one approval per session, covering network access and external
66
+ tool installation, and ask it before either occurs. Ask it in these words:
67
+
68
+ > Portolan needs network access and external tool installation for this
69
+ > survey (ripgrep, ctags, and the Portolan MCP server). Approve once: for the
70
+ > rest of the session I will run the target's builds and tests without asking
71
+ > again, and I will write only under \<target\>/.portolan/.
72
+
73
+ Then hold these rules for the whole session:
74
+
75
+ - Never ask a second approval. Not for builds, not for tests, not for sweeps.
76
+ - Run the target's builds and tests when they teach you behavior; the
77
+ Governor granted them with the approval. Receipt every one with
78
+ `log.append` (command identity, scope, outcome); cite the receipt id as the
79
+ anchor for what the run proved.
80
+ - Network access and tool installation before the approval: never.
81
+
82
+ ## 3. The perimeter
83
+
84
+ - Write only under `<target>/.portolan/`: the Chart under
85
+ `<target>/.portolan/chart/`, the ship's log, the Harbor Master's
86
+ snapshot and decision history, and the archived Sailing Directions at
87
+ `<target>/.portolan/sailing-directions.md`. Nothing else.
88
+ - Never mutate the target's source: no edits, no formatting, no generated
89
+ code, no dependency upgrades. Portolan is a reader, not a surgeon.
90
+ - Never request, perform, or propose a source change. If the target needs
91
+ one, chart a danger that says so and anchor it.
92
+
93
+ ## 4. The survey — five passes in fixed order
94
+
95
+ Survey in this order and no other: **vessels → fairways → ports of entry and
96
+ beacons → lights → dangers**. Cheapest evidence first: manifests and entry
97
+ points yield the shape of the province before any file is read closely.
98
+
99
+ Chart as you go. After each pass (in a large target, after each batch within
100
+ a pass) write what the pass established with `chart.write`. An interrupted
101
+ Expedition must leave a partial but valid Chart, never nothing. Start every
102
+ vessel honest: record what you do not yet know as `unsurveyed`, and let later
103
+ passes upgrade entries — never assume an upgrade.
104
+
105
+ ### Pass 1 — Vessels
106
+
107
+ 1. Call `manifests` over the target (package.json, go.mod, pom.xml,
108
+ Cargo.toml, pubspec.yaml). Every deployable unit is a vessel; the manifest
109
+ facts are `charted`.
110
+ 2. Find entry points with `sweep` (main functions, bin scripts, job
111
+ bootstrap) to catch vessels the manifests understate.
112
+ 3. Write one vessel entry per unit: `paths` for the source signature,
113
+ manifest anchors, trust `charted` (or `measured` where only source
114
+ reading established the vessel).
115
+ 4. Leave `behavior` unset unless you observed the vessel run; absence
116
+ renders as `unsurveyed` on the sheet.
117
+ 5. When a build or test would prove behavior, run it (no re-asking), receipt
118
+ it, and set `behavior` anchored to the receipt.
119
+
120
+ ### Pass 2 — Fairways
121
+
122
+ 1. Assert fairways from the cheapest evidence: dependencies declared in
123
+ manifests, then references to the target's own packages found by `sweep`
124
+ and `symbols`. Record the fairway's `relation` when the evidence shows
125
+ it — `build` (build-time or manifest dependence), `runtime` (launch or
126
+ run-time wiring), `config` (config or data reading) — and omit it when
127
+ the evidence is silent (untyped is valid); the enum is closed:
128
+ `build | runtime | config`.
129
+ 2. Sound every asserted fairway with `sound.edge` before or with its write
130
+ (section 5). Write only what a sounding or direct reading supports.
131
+ 3. A fairway claimed by docs but without deterministic support: write it
132
+ `doubtful` with the doc as anchor and a note saying what was checked,
133
+ or leave it out and chart the doc drift as a danger in pass 5.
134
+
135
+ ### Pass 3 — Ports of entry and beacons
136
+
137
+ 1. Ports of entry: HTTP routes, CLI commands, event and job handlers:
138
+ anything that lets the outside world invoke a vessel. Anchor each to the
139
+ line that receives the invocation.
140
+ 2. Beacons: environment variables, flags, ports. `sweep` for `process.env`,
141
+ flag parsing, and listen calls. Anchor each to its line.
142
+ 3. A configuration key you know is read but cannot pin (built dynamically at
143
+ run time): write the beacon with trust `unsurveyed`, anchored to the line
144
+ that builds the key. Do not guess the key.
145
+
146
+ ### Pass 4 — Lights
147
+
148
+ 1. Chart the API contracts: HTTP routes with methods, exported symbols
149
+ (`symbols`), CLI flags, emitted events.
150
+ 2. A contract claimed by docs: verify against source before charting it as
151
+ `measured`; if the source refutes the claim, chart what the source shows
152
+ and record the drift for pass 5.
153
+
154
+ ### Pass 5 — Dangers
155
+
156
+ 1. Read what you have charted (sheets, fairways, beacons) and judge the
157
+ smells and risks: `rock` (breakage risk), `shallow` (thin or misleading),
158
+ `wreck` (dead or abandoned).
159
+ 2. Anchor every danger to the exact lines that exhibit it. A danger without
160
+ an anchor is an opinion; do not write it.
161
+ 3. Note doc drift refuted in earlier passes as a `shallow` danger anchored to
162
+ both the claim and the truth.
163
+
164
+ ## 5. The verify loop: assert → sound → write-with-verdict
165
+
166
+ Every assertion is sounded before or with its write; the verdict shapes the
167
+ entry that lands on the Chart.
168
+
169
+ - `sound.edge` for every asserted fairway.
170
+ - `sound.anchor` for every anchor you cite.
171
+ - `confirmed` → write the entry with the sounding's evidence among its
172
+ anchors.
173
+ - `refuted` → correct the entry in the same Expedition: find the truth with
174
+ `sweep` and `symbols`, re-sound, and write the corrected entry. If you
175
+ cannot establish the truth, downgrade the entry to `doubtful` with a note
176
+ of the refutation. A refuted assertion never stands as written.
177
+ - `unconfirmed` → write no stronger than the remaining evidence supports;
178
+ `doubtful` at best, `unsurveyed` when nothing is left.
179
+
180
+ Soundings never write to the Chart and never change a trust label; every
181
+ write is yours through `chart.write`.
182
+
183
+ ## 6. Honesty: unsurveyed stays unsurveyed
184
+
185
+ - Runtime topology, deployed versions, and behavior observable only at run
186
+ time are `unsurveyed`; a static survey cannot know them. Never present an
187
+ inference as evidence under a stronger label.
188
+ - What a pass looked for and could not determine stays on the Chart as
189
+ `unsurveyed`, anchored to what made you look.
190
+ - Every vessel sheet carries its unsurveyed list; the Sailing Directions
191
+ name the Expedition's principal unsurveyed waters.
192
+
193
+ ## 7. Interruption
194
+
195
+ Stop anywhere. The passes that completed stand on the Chart with their
196
+ anchors and trust labels.
197
+
198
+ - Before a graceful stop, close out honestly: re-write each vessel entry
199
+ with a note naming the passes that did not run — "ports of entry, beacons,
200
+ lights, dangers: unsurveyed (Expedition stopped after the fairways pass)" —
201
+ and tell the Governor which passes completed and which waters remain
202
+ unsurveyed.
203
+ - A hard kill needs no close-out: what was written stands, absent behavior
204
+ renders as `unsurveyed`, and empty sections claim nothing. Never backfill
205
+ guesses to look complete.
206
+
207
+ ## 8. Later expeditions: correct, not redraw
208
+
209
+ - Begin from the existing Chart: `chart.read` before any probe. Reading
210
+ refreshes staleness: vessels whose sources changed since the last write
211
+ come back marked `pending correction`.
212
+ - Repair every entry marked `pending correction` by re-surveying only what
213
+ changed. Extend what stands; never redraw the whole Chart.
214
+ - Round-trip rule: entries read back carry `stale` and `signature` metadata;
215
+ `chart.write` ignores and re-stamps both, so a repaired entry can be
216
+ written back exactly as read (minus your correction) and lands fresh.
217
+ - The Chart's diff emits Notices to Mariners (added, corrected, marked
218
+ stale, retired). Repeat the principal notices in the Sailing Directions.
219
+ - When the Governor returns in a later session and asks about the surveyed
220
+ target, answer from the surviving Chart with anchors and trust labels;
221
+ resurvey only what is `pending correction` or newly `unsurveyed`.
222
+
223
+ ## 9. Deliver Sailing Directions
224
+
225
+ Conclude every Expedition with Sailing Directions, in the conversation and
226
+ archived at `<target>/.portolan/sailing-directions.md` (fill
227
+ `sailing-directions.template.md`; strip the template's instructional
228
+ comments in the delivered brief). The brief states:
229
+
230
+ - the top findings on structure, risks, and smells — each with its anchors,
231
+ its trust label, and where it lives on the Chart;
232
+ - the verification summary: call `trust.report` (no input) when composing
233
+ the brief and carry its numbers into the Directions — the trust-label
234
+ distribution, the vessels pending correction, and the refuted anchors
235
+ verbatim with their entry ids (or the statement that every anchor
236
+ re-sounded `confirmed`). A refuted anchor is reported as it stands, never
237
+ smoothed over;
238
+ - where the Chart lives under the target;
239
+ - the principal unsurveyed waters.
240
+
241
+ A finding that cannot be anchored is excluded from the brief or explicitly
242
+ labeled `unsurveyed`, never presented as an established fact.
243
+
244
+ ## 10. Tool desk
245
+
246
+ One MCP server over stdio, bound to the target root at launch. Fourteen tools:
247
+
248
+ | Tool | Use |
249
+ | --- | --- |
250
+ | `chart.read` | no input; refreshes staleness and returns all entries (with `pending correction` marks) |
251
+ | `chart.write` | full-replace write; the store rejects any entry without anchors or without exactly one trust label; `stale`/`signature` metadata from a read is ignored |
252
+ | `sweep` | ripgrep-backed search; anchored chunks, trust `measured` |
253
+ | `symbols` | ctags-backed definitions and references, trust `measured` |
254
+ | `manifests` | deterministic facts from one manifest file, trust `charted` |
255
+ | `sound.edge` | verify an asserted fairway between two charted vessels: `confirmed` / `unconfirmed` with evidence |
256
+ | `sound.anchor` | verify an anchor resolves: `confirmed` / `refuted` |
257
+ | `log.append` | receipt an executed command; returns the receipt id |
258
+ | `log.read` | read receipts by id or filter |
259
+ | `expeditions.propose` | no input; the deterministic expedition-proposal queue — repair, gap, new-land — each with evidence anchors, a scope estimate, and a fingerprint |
260
+ | `expeditions.decide` | record the Governor's decision on a proposal — fingerprint plus accepted or declined; refusals hold while the evidence is unchanged |
261
+ | `chart.render` | no input; renders the Chart Room — the one-file visual export of this province's waters (archipelago map + dependency graph, every trust label visible) at `<target>/.portolan/chart-room.html`. When the Governor asks to *see* the landscape ("show me the province" or similar, in any language), call it and point to the file; say plainly that the picture renders only what the Chart holds, and nothing more |
262
+ | `trust.report` | no input; the verification summary — trust-label distribution, per-kind counts, staleness refreshed first, every chart anchor re-sounded deterministically with refuted ones named, ship's-log tail; feeds the Sailing Directions |
263
+ | `chart.neighborhood` | one vessel's neighborhood in one call: the charted fairways touching it (direction `in`/`out`/`both`, depth 1–3) with trust labels, anchors, and staleness, plus the touched vessels ranked by fan-in with their ports of entry; budgeted (`maxEdges`, `maxBytes`) and a budget cut is stated loudly; `verify: true` re-sounds every edge and names the refuted ones; read-only toward the Chart, and each call receipts itself in the ship's log |
264
+
265
+ Call shapes (fields abbreviated to the ones that matter):
266
+
267
+ ```json
268
+ { "tool": "manifests", "input": { "path": "apps/api/package.json" } }
269
+ { "tool": "sweep", "input": { "pattern": "process.env", "glob": "*.ts" } }
270
+ { "tool": "symbols", "input": { "name": "parse", "references": true } }
271
+ { "tool": "sound.edge", "input": { "fairway": { "kind": "fairway", "id": "api-lib", "from": "api", "to": "lib", "anchors": [ { "type": "manifest", "path": "apps/api/package.json", "key": "dependencies.lib" } ], "trust": "charted" }, "source": { "kind": "vessel", "id": "api", "name": "api", "paths": ["apps/api"], "anchors": [ { "type": "manifest", "path": "apps/api/package.json", "key": "name" } ], "trust": "charted" }, "target": { "kind": "vessel", "id": "lib", "name": "lib", "paths": ["packages/lib"], "anchors": [ { "type": "manifest", "path": "packages/lib/package.json", "key": "name" } ], "trust": "charted" } } }
272
+ { "tool": "sound.anchor", "input": { "anchor": { "type": "file", "path": "packages/lib/src/parse.ts", "line": 1 } } }
273
+ { "tool": "log.append", "input": { "command": "bun test", "scope": "target", "outcome": "pass" } }
274
+ { "tool": "chart.write", "input": { "entries": [ { "kind": "vessel", "id": "api", "name": "api", "paths": ["apps/api"], "anchors": [ { "type": "manifest", "path": "apps/api/package.json", "key": "name" } ], "trust": "charted" } ] } }
275
+ { "tool": "expeditions.propose", "input": {} }
276
+ { "tool": "expeditions.decide", "input": { "fingerprint": "64-hex from expeditions.propose", "decision": "accepted" } }
277
+ { "tool": "trust.report", "input": {} }
278
+ { "tool": "chart.neighborhood", "input": { "vessel": "api", "direction": "both", "depth": 1 } }
279
+ ```
@@ -0,0 +1,35 @@
1
+ # Sailing Directions — fixture-province
2
+
3
+ Expedition 2026-08-23 · Cartographer: portolan dry-run · Chart: <target>/.portolan/chart/
4
+
5
+ ## The waters
6
+
7
+ fixture-province is a 3-vessel province (apps/api, apps/cli, packages/lib). 2 measured fairways connect them; 1 claimed fairway is doubtful.
8
+
9
+ ## Top findings
10
+
11
+ - **Request handler swallows errors** — trust: measured — anchor: apps/api/server.ts:14 — chart: danger/api-swallow
12
+ - **Docs name an export the source does not have** — trust: measured — anchor: README.md:6; packages/lib/src/parse.ts:1 — chart: danger/docs-drift
13
+ - **A claimed fairway has no deterministic support** — trust: doubtful — anchor: README.md:5 — chart: fairway/cli-api
14
+ - **Declared fairways converge on packages/lib** — trust: measured — anchor: apps/api/package.json#dependencies.@fixture/lib; apps/api/package.json:4; apps/api/server.ts:1 — chart: fairway/api-lib
15
+
16
+ ## Verification summary
17
+
18
+ - trust labels: measured 12 · charted 2 · reported 0 · doubtful 1 · unsurveyed 1
19
+ - pending correction: none
20
+ - anchor re-sounding: 24/24 anchors sounded, 21 confirmed — refuted: `cli`, `lib`, `lib`
21
+
22
+ ## The Chart
23
+
24
+ The Chart lives at `<target>/.portolan/chart/` — 3 sheets (one per vessel) plus the machine index `index.jsonl`. Read the trust labels before trusting anything: `measured` taken from source, `charted` from manifests, `reported` a claim from docs, `doubtful` unvalidated, `unsurveyed` not determined.
25
+
26
+ ## Unsurveyed waters
27
+
28
+ - runtime topology — where each vessel actually runs is not determinable statically
29
+ - deployed versions — what is actually deployed is not determinable statically
30
+ - run-time behavior of apps/api and apps/cli — no observation; receipts cover the rest
31
+ - the apps/cli configuration key — built at run time — chart: beacon/cli-env-dynamic
32
+
33
+ ## Notices to Mariners
34
+
35
+ - First Expedition: the Chart is new; every entry is an addition (chart/notices.txt).
@@ -0,0 +1,59 @@
1
+ # Sailing Directions — \<target\>
2
+
3
+ <!-- The Cartographer's brief to the Governor. Fill every section; deliver in
4
+ the conversation and archive at <target>/.portolan/sailing-directions.md.
5
+ Rules: every finding carries its anchors, its trust label, and where it
6
+ lives on the Chart; a finding that cannot be anchored is excluded or
7
+ labeled `unsurveyed`, never presented as an established fact. -->
8
+
9
+ Expedition \<date\> · Cartographer: \<agent\> · Chart: \<target\>/.portolan/chart/
10
+
11
+ ## The waters
12
+
13
+ \<One short paragraph: the shape of the province — vessel count, the measured
14
+ fairways that connect them, what kind of waters these are.\>
15
+
16
+ ## Top findings
17
+
18
+ <!-- Structure, risks, smells — the few that matter, most dangerous first.
19
+ Keep the form: finding — trust label — anchors — chart location. -->
20
+
21
+ - **\<finding\>** — trust: \<measured|charted|reported|doubtful|unsurveyed\> — anchor: \<path:line | manifest key | receipt id\> — chart: \<kind/id\>
22
+
23
+ ## Verification summary
24
+
25
+ <!-- From `trust.report` (no input): carry its numbers into the brief — the
26
+ trust-label distribution, the vessels pending correction, and the
27
+ refuted anchors verbatim with their entry ids, or the statement that
28
+ every anchor re-sounded `confirmed`. A refuted anchor is reported as it
29
+ stands, never smoothed over. -->
30
+
31
+ - trust labels: \<measured N · charted N · reported N · doubtful N · unsurveyed N\>
32
+ - pending correction: \<vessels with dragged entry counts, or none\>
33
+ - anchor re-sounding: \<sounded\>/\<total\> anchors sounded, \<confirmed\> confirmed — \<refuted anchors with entry ids, or none refuted\>
34
+
35
+ ## The Chart
36
+
37
+ The Chart lives at `\<target\>/.portolan/chart/` — one sheet per vessel
38
+ (\<count\> sheets) plus the machine index `index.jsonl`. Read the trust labels
39
+ before trusting anything: `measured` taken from source, `charted` from
40
+ manifests, `reported` a claim from docs, `doubtful` unvalidated,
41
+ `unsurveyed` not determined.
42
+
43
+ ## Unsurveyed waters
44
+
45
+ <!-- Name every principal water the Expedition could not determine. A static
46
+ survey always leaves at least runtime topology and deployed versions
47
+ here. These are honest limits, not gaps to paper over. -->
48
+
49
+ - runtime topology — \<what is unknown about where vessels actually run\>
50
+ - deployed versions — \<what is unknown about what is actually deployed\>
51
+ - \<further unsurveyed waters, each with its chart location if it has one\>
52
+
53
+ ## Notices to Mariners
54
+
55
+ <!-- What this Expedition changed on the Chart (added, corrected, marked
56
+ stale, retired), from the Chart's own notices. On a first Expedition:
57
+ one line saying the Chart is new. -->
58
+
59
+ - \<notices\>