@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.
- package/LICENSE +21 -0
- package/README.md +110 -0
- package/adapters/README.md +226 -0
- package/adapters/omp/portolan-mcp +19 -0
- package/adapters/opencode/expedition-launcher +70 -0
- package/adapters/opencode/install.test.ts +105 -0
- package/adapters/opencode/install.ts +357 -0
- package/adapters/pi/portolan-mcp +19 -0
- package/adapters/scheduling/night-watch.cron +23 -0
- package/core/schema/chart.schema.json +154 -0
- package/core/src/bin/portolan.ts +84 -0
- package/core/src/chart-io.rollback-fixture.ts +55 -0
- package/core/src/chart-io.ts +121 -0
- package/core/src/chart-store.ts +137 -0
- package/core/src/chartroom/cli.ts +63 -0
- package/core/src/chartroom/render.ts +213 -0
- package/core/src/chartroom/review-template.html +232 -0
- package/core/src/chartroom/review.ts +109 -0
- package/core/src/chartroom/template.html +1090 -0
- package/core/src/fan-in.ts +84 -0
- package/core/src/harbor/chat-format.ts +154 -0
- package/core/src/harbor/cli.ts +178 -0
- package/core/src/harbor/errors.ts +22 -0
- package/core/src/harbor/fingerprint.ts +29 -0
- package/core/src/harbor/history.ts +178 -0
- package/core/src/harbor/launcher.ts +155 -0
- package/core/src/harbor/night-policy.ts +64 -0
- package/core/src/harbor/proposals.ts +324 -0
- package/core/src/harbor/run.ts +72 -0
- package/core/src/harbor/settings.ts +108 -0
- package/core/src/harbor/snapshot.ts +187 -0
- package/core/src/harbor/watch.ts +103 -0
- package/core/src/index.ts +28 -0
- package/core/src/notices.ts +117 -0
- package/core/src/perimeter.ts +44 -0
- package/core/src/server/adapter-boundary.ts +66 -0
- package/core/src/server/main.ts +27 -0
- package/core/src/server/registry.ts +609 -0
- package/core/src/server/server.ts +123 -0
- package/core/src/server/test-harness.ts +161 -0
- package/core/src/sheets.ts +151 -0
- package/core/src/staleness.ts +203 -0
- package/core/src/tools/log.ts +215 -0
- package/core/src/tools/manifests.ts +912 -0
- package/core/src/tools/neighborhood.ts +423 -0
- package/core/src/tools/shared.ts +72 -0
- package/core/src/tools/sound.ts +634 -0
- package/core/src/tools/sweep.ts +198 -0
- package/core/src/tools/symbols.ts +176 -0
- package/core/src/tools/trust-report.ts +193 -0
- package/core/src/types.ts +162 -0
- package/core/src/validate.ts +106 -0
- package/package.json +34 -0
- package/skill/SKILL.md +279 -0
- package/skill/examples/sailing-directions-example.md +35 -0
- package/skill/sailing-directions.template.md +59 -0
- package/skill/verify/checks.ts +476 -0
- package/skill/verify/dry-run.ts +738 -0
- 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\>
|