@north-light/crouter-api 0.3.180 → 0.3.181
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/README.md +9 -2
- package/dist/{client.d.ts → api/client.d.ts} +10 -1
- package/dist/{client.js → api/client.js} +13 -0
- package/dist/{dto → api/dto}/broker.d.ts +32 -0
- package/dist/{dto → api/dto}/crons.d.ts +17 -0
- package/dist/api/dto/memory.d.ts +17 -0
- package/dist/api/dto/memory.js +6 -0
- package/dist/{dto → api/dto}/messages.d.ts +5 -0
- package/dist/{dto → api/dto}/reviews.d.ts +8 -4
- package/dist/{index.d.ts → api/index.d.ts} +1 -0
- package/dist/{index.js → api/index.js} +1 -0
- package/dist/{routes.d.ts → api/routes.d.ts} +2 -0
- package/dist/{routes.js → api/routes.js} +4 -0
- package/dist/core/memory/doc-link-grammar.d.ts +20 -0
- package/dist/core/memory/doc-link-grammar.js +113 -0
- package/package.json +13 -7
- /package/dist/{__tests__ → api/__tests__}/serial/client.test.d.ts +0 -0
- /package/dist/{__tests__ → api/__tests__}/serial/client.test.js +0 -0
- /package/dist/{dto → api/dto}/attach.d.ts +0 -0
- /package/dist/{dto → api/dto}/attach.js +0 -0
- /package/dist/{dto → api/dto}/broker-ops.d.ts +0 -0
- /package/dist/{dto → api/dto}/broker-ops.js +0 -0
- /package/dist/{dto → api/dto}/broker.js +0 -0
- /package/dist/{dto → api/dto}/canvas.d.ts +0 -0
- /package/dist/{dto → api/dto}/canvas.js +0 -0
- /package/dist/{dto → api/dto}/common.d.ts +0 -0
- /package/dist/{dto → api/dto}/common.js +0 -0
- /package/dist/{dto → api/dto}/config.d.ts +0 -0
- /package/dist/{dto → api/dto}/config.js +0 -0
- /package/dist/{dto → api/dto}/crons.js +0 -0
- /package/dist/{dto → api/dto}/files.d.ts +0 -0
- /package/dist/{dto → api/dto}/files.js +0 -0
- /package/dist/{dto → api/dto}/focus.d.ts +0 -0
- /package/dist/{dto → api/dto}/focus.js +0 -0
- /package/dist/{dto → api/dto}/health.d.ts +0 -0
- /package/dist/{dto → api/dto}/health.js +0 -0
- /package/dist/{dto → api/dto}/human.d.ts +0 -0
- /package/dist/{dto → api/dto}/human.js +0 -0
- /package/dist/{dto → api/dto}/inbox.d.ts +0 -0
- /package/dist/{dto → api/dto}/inbox.js +0 -0
- /package/dist/{dto → api/dto}/lifecycle.d.ts +0 -0
- /package/dist/{dto → api/dto}/lifecycle.js +0 -0
- /package/dist/{dto → api/dto}/messages.js +0 -0
- /package/dist/{dto → api/dto}/modelauth.d.ts +0 -0
- /package/dist/{dto → api/dto}/modelauth.js +0 -0
- /package/dist/{dto → api/dto}/nodes.d.ts +0 -0
- /package/dist/{dto → api/dto}/nodes.js +0 -0
- /package/dist/{dto → api/dto}/profiles.d.ts +0 -0
- /package/dist/{dto → api/dto}/profiles.js +0 -0
- /package/dist/{dto → api/dto}/reports.d.ts +0 -0
- /package/dist/{dto → api/dto}/reports.js +0 -0
- /package/dist/{dto → api/dto}/review-comments.d.ts +0 -0
- /package/dist/{dto → api/dto}/review-comments.js +0 -0
- /package/dist/{dto → api/dto}/reviews.js +0 -0
- /package/dist/{dto → api/dto}/subscriptions.d.ts +0 -0
- /package/dist/{dto → api/dto}/subscriptions.js +0 -0
- /package/dist/{dto → api/dto}/worktree.d.ts +0 -0
- /package/dist/{dto → api/dto}/worktree.js +0 -0
- /package/dist/{errors.d.ts → api/errors.d.ts} +0 -0
- /package/dist/{errors.js → api/errors.js} +0 -0
package/README.md
CHANGED
|
@@ -41,11 +41,18 @@ autostart hook.
|
|
|
41
41
|
canvas, worktree, and human-in-the-loop.
|
|
42
42
|
- **`routes` / `API_VERSION`** — pure path builders for the `/v1` surface.
|
|
43
43
|
- **`ApiError` / `ErrorBody` / `isErrorBody`** — the error contract.
|
|
44
|
+
- **`@north-light/crouter-api/doc-link-grammar`** — the `[[canonical/name]]`
|
|
45
|
+
memory-document link grammar (`findDocLinks`, `docLinkNames`,
|
|
46
|
+
`isDocLinkName`). Zero imports and browser-safe, so a UI can scan transcript
|
|
47
|
+
markdown for doc links in the renderer and hand each name to
|
|
48
|
+
`client.resolveMemoryDoc(name, nodeId)`. Import the subpath, not the root, in
|
|
49
|
+
a browser bundle — the root pulls `CrtrClient` and its Node built-ins.
|
|
44
50
|
|
|
45
51
|
## Source of truth & releases
|
|
46
52
|
|
|
47
53
|
The source lives in the [`crouter`](https://github.com/vallum-security/crouter)
|
|
48
|
-
repo at `src/api
|
|
49
|
-
|
|
54
|
+
repo at `src/api/`, plus `src/core/memory/doc-link-grammar.ts` for the subpath
|
|
55
|
+
above; this package is a build-and-publish mirror of those files (it carries no
|
|
56
|
+
source of its own). It is published by the `publish-api.yml`
|
|
50
57
|
GitHub Action when a tag matching `api-vX.Y.Z` is pushed — the tag version must
|
|
51
58
|
match this package's `version`.
|
|
@@ -5,11 +5,12 @@ import type { PushReportRequest, PushReportResultDTO, ReportDTO, ReportsQuery }
|
|
|
5
5
|
import type { CloseRequest, CloseResultDTO, PromoteRequest, RelaunchRootResultDTO, ReviveRequest, ReviveResultDTO, WaitRequest, YieldRequest } from './dto/lifecycle.js';
|
|
6
6
|
import type { SubscribeRequest, SubscriptionDTO } from './dto/subscriptions.js';
|
|
7
7
|
import type { FocusDTO, RegisterFocusRequest, SetFocusPaneRequest } from './dto/focus.js';
|
|
8
|
-
import { type ArmCronRequest, type CancelCronQuery, type CronDTO, type CronRunDTO, type CronScopeQuery, type CronShowDTO, type ListCronsQuery } from './dto/crons.js';
|
|
8
|
+
import { type ArmCronRequest, type CancelCronQuery, type CronDTO, type CronRunDTO, type CronScopeQuery, type CronShowDTO, type ListCronsQuery, type PokeCronsResult } from './dto/crons.js';
|
|
9
9
|
import type { NodeConfigPatch } from './dto/config.js';
|
|
10
10
|
import type { AttachEnsureRequest, AttachEnsureResultDTO } from './dto/attach.js';
|
|
11
11
|
import type { EnsureProfileRequest, ProfileDTO } from './dto/profiles.js';
|
|
12
12
|
import type { FilePeekDTO } from './dto/files.js';
|
|
13
|
+
import type { MemoryDocRefDTO } from './dto/memory.js';
|
|
13
14
|
import type { CredentialRemovalResultDTO, CredentialResultDTO, InstallCredentialRequest, ModelAuthListDTO } from './dto/modelauth.js';
|
|
14
15
|
import type { CreateHumanBridgeRequest, HumanBridgeResultDTO, HumanCancelRequest, HumanCancelResultDTO, HumanResolveRequest, HumanResolveResultDTO } from './dto/human.js';
|
|
15
16
|
import type { CancelReviewRequest, CreateReviewRequest, ListReviewsQuery, ReviewCancelResultDTO, ReviewDocumentBaseDTO, ReviewDTO, ReviewListDTO, ReviewSubmitResultDTO } from './dto/reviews.js';
|
|
@@ -131,6 +132,10 @@ export declare class CrtrClient {
|
|
|
131
132
|
runCron(cronId: string, q?: CronScopeQuery): Promise<CronRunDTO>;
|
|
132
133
|
/** Cancel one cron (`DELETE /v1/crons/:cronId`, idempotent). */
|
|
133
134
|
cancelCron(cronId: string, q?: CancelCronQuery): Promise<void>;
|
|
135
|
+
/** Bare eligibility poke (`POST /v1/crons/poke`): re-dues every held active
|
|
136
|
+
* cron now — "something changed; re-check now". Canvas-wide, label-free,
|
|
137
|
+
* idempotent, and free when nothing is held. */
|
|
138
|
+
pokeCrons(): Promise<PokeCronsResult>;
|
|
134
139
|
ensureAttach(id: string, req?: AttachEnsureRequest): Promise<AttachEnsureResultDTO>;
|
|
135
140
|
getReports(id: string, q?: ReportsQuery): Promise<ReportDTO[]>;
|
|
136
141
|
getTranscript(id: string, q?: TranscriptQuery): Promise<TranscriptDTO>;
|
|
@@ -143,6 +148,10 @@ export declare class CrtrClient {
|
|
|
143
148
|
/** Read an absolute host path as UTF-8 (capped, `truncated` when clipped) for
|
|
144
149
|
* the browser file-peek panel. */
|
|
145
150
|
peekFile(path: string): Promise<FilePeekDTO>;
|
|
151
|
+
/** Resolve a `[[name]]` memory-document link to the absolute path the given
|
|
152
|
+
* node would read — the node's own precedence chain, not this process's.
|
|
153
|
+
* Pair with `peekFile` to render the document. */
|
|
154
|
+
resolveMemoryDoc(name: string, nodeId: string): Promise<MemoryDocRefDTO>;
|
|
146
155
|
ensureProfile(name: string, req?: EnsureProfileRequest): Promise<ProfileDTO>;
|
|
147
156
|
listProfiles(): Promise<ProfileDTO[]>;
|
|
148
157
|
getProfile(name: string): Promise<ProfileDTO>;
|
|
@@ -227,6 +227,12 @@ export class CrtrClient {
|
|
|
227
227
|
async cancelCron(cronId, q) {
|
|
228
228
|
await this.request('DELETE', withQuery(routes.cron(this.cronPath(cronId)), q));
|
|
229
229
|
}
|
|
230
|
+
/** Bare eligibility poke (`POST /v1/crons/poke`): re-dues every held active
|
|
231
|
+
* cron now — "something changed; re-check now". Canvas-wide, label-free,
|
|
232
|
+
* idempotent, and free when nothing is held. */
|
|
233
|
+
pokeCrons() {
|
|
234
|
+
return this.request('POST', routes.cronsPoke(), {});
|
|
235
|
+
}
|
|
230
236
|
ensureAttach(id, req) {
|
|
231
237
|
return this.request('POST', routes.nodeAttach(this.nodePath(id)), req ?? {});
|
|
232
238
|
}
|
|
@@ -257,6 +263,13 @@ export class CrtrClient {
|
|
|
257
263
|
peekFile(path) {
|
|
258
264
|
return this.request('GET', withQuery(routes.filePeek(), { path }));
|
|
259
265
|
}
|
|
266
|
+
// ---- Memory documents --------------------------------------------------
|
|
267
|
+
/** Resolve a `[[name]]` memory-document link to the absolute path the given
|
|
268
|
+
* node would read — the node's own precedence chain, not this process's.
|
|
269
|
+
* Pair with `peekFile` to render the document. */
|
|
270
|
+
resolveMemoryDoc(name, nodeId) {
|
|
271
|
+
return this.request('GET', withQuery(routes.memoryResolve(), { name, node: nodeId }));
|
|
272
|
+
}
|
|
260
273
|
// ---- Profiles ----------------------------------------------------------
|
|
261
274
|
ensureProfile(name, req) {
|
|
262
275
|
return this.request('PUT', routes.profile(name), req ?? {});
|
|
@@ -28,6 +28,38 @@ export interface BrokerWelcomeSnapshot<M = unknown> {
|
|
|
28
28
|
export interface BrokerWelcomeFrame<M = unknown> {
|
|
29
29
|
type: 'welcome';
|
|
30
30
|
snapshot?: BrokerWelcomeSnapshot<M>;
|
|
31
|
+
/** The last few id-bearing user-message dispatches from the broker's message
|
|
32
|
+
* ledger, oldest first — so a reattaching follower can key the snapshot's
|
|
33
|
+
* trailing user wakes by id (the id a sender minted via `message_id` on
|
|
34
|
+
* `POST .../messages`) instead of discarding them heuristically. Live-process
|
|
35
|
+
* memory only: a broker restart loses it. Absent on a broker pinned to an
|
|
36
|
+
* older runtime generation. */
|
|
37
|
+
recentUserMessages?: Array<{
|
|
38
|
+
id: string;
|
|
39
|
+
text: string;
|
|
40
|
+
}>;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The additive identity fields the broker sets on a relayed pi `queue_update`:
|
|
44
|
+
* id arrays PARALLEL to pi's text arrays (`steeringIds[i]` identifies
|
|
45
|
+
* `steering[i]`). Sender-minted when the send carried `message_id`,
|
|
46
|
+
* broker-minted otherwise. Absent on a broker pinned to an older runtime
|
|
47
|
+
* generation — consumers fall back to bare texts. Intersect with the pinned pi
|
|
48
|
+
* `queue_update` event shape to read them typed.
|
|
49
|
+
*/
|
|
50
|
+
export interface RelayedQueueUpdateIdentity {
|
|
51
|
+
steeringIds?: string[];
|
|
52
|
+
followUpIds?: string[];
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The additive identity field the broker sets on a relayed USER-role
|
|
56
|
+
* `message_start`: the message's crouter id — the `message_id` its sender
|
|
57
|
+
* minted, or a broker-minted one. Absent when the message carries none (an
|
|
58
|
+
* engine-command expansion, a pre-ledger replay, an older broker). Intersect
|
|
59
|
+
* with the pinned pi `message_start` event shape to read it typed.
|
|
60
|
+
*/
|
|
61
|
+
export interface RelayedUserMessageStartIdentity {
|
|
62
|
+
crtrMessageId?: string;
|
|
31
63
|
}
|
|
32
64
|
/**
|
|
33
65
|
* The broker-control frames a relay consumer reads: `welcome` (catch-up snapshot)
|
|
@@ -53,6 +53,14 @@ export interface CronDTO {
|
|
|
53
53
|
sink: string | null;
|
|
54
54
|
tier: string;
|
|
55
55
|
state: CronStateDTO;
|
|
56
|
+
/** True while the row is parked by the exit-75 owed-gate disposition: the
|
|
57
|
+
* last scheduled run declared "owed but not currently eligible", so the
|
|
58
|
+
* occurrence was not spent. A daemon poke re-dues it now; otherwise a
|
|
59
|
+
* recurring row re-checks at its natural `fire_at` slot (the backstop) and
|
|
60
|
+
* a held one-shot waits for a poke until `expires_at` deletes it. `state`
|
|
61
|
+
* stays honest (active|paused) — renderers derive; an active held row must
|
|
62
|
+
* never present as paused. */
|
|
63
|
+
held: boolean;
|
|
56
64
|
run_state: CronRunStateDTO;
|
|
57
65
|
/** Recent health: the most recent settled run, or null if it never ran. */
|
|
58
66
|
last_run: CronLastRunDTO | null;
|
|
@@ -93,6 +101,15 @@ export interface CronRunDTO {
|
|
|
93
101
|
/** What the sink did with this run's output, or why it didn't. */
|
|
94
102
|
delivered: string | null;
|
|
95
103
|
}
|
|
104
|
+
/** `POST /v1/crons/poke` — the bare daemon-level eligibility poke. Re-dues
|
|
105
|
+
* every held active cron now; paused rows keep their held state. Idempotent
|
|
106
|
+
* and free when nothing is held. */
|
|
107
|
+
export interface PokeCronsResult {
|
|
108
|
+
/** How many held rows were re-dued — for the caller's log line. */
|
|
109
|
+
unparked: number;
|
|
110
|
+
/** The poke receipt instant (UTC). */
|
|
111
|
+
at: IsoTime;
|
|
112
|
+
}
|
|
96
113
|
/** `GET /v1/crons/:cronId` — one cron with its run-log ring (most recent first). */
|
|
97
114
|
export interface CronShowDTO {
|
|
98
115
|
cron: CronDTO;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** `GET /v1/memory/resolve?name=<name>&node=<id>` result — where the named
|
|
2
|
+
* document lives for that node. Resolution runs the node's own precedence
|
|
3
|
+
* chain (its context store, its project stack, its profile, user, builtin), so
|
|
4
|
+
* the same name can answer with different documents for different nodes. */
|
|
5
|
+
export interface MemoryDocRefDTO {
|
|
6
|
+
/** The document's canonical identity (its frontmatter `name`, else its
|
|
7
|
+
* path-derived name) — which may differ from the queried name when the query
|
|
8
|
+
* was a bare leaf or a directory whose INDEX resolved. */
|
|
9
|
+
name: string;
|
|
10
|
+
/** Which store it came from: node | project | profile | user | builtin. */
|
|
11
|
+
scope: string;
|
|
12
|
+
/** Absolute path to the `.md` file. */
|
|
13
|
+
path: string;
|
|
14
|
+
/** The owning plugin's name when the doc is mounted from an installed plugin,
|
|
15
|
+
* absent for a native scope doc. */
|
|
16
|
+
plugin?: string;
|
|
17
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Memory-document resolution DTO. Backs `GET /v1/memory/resolve` — a client
|
|
2
|
+
// holding a `[[name]]` link out of a node's transcript turns it into the
|
|
3
|
+
// absolute path of the document that node would read, then peeks that path.
|
|
4
|
+
//
|
|
5
|
+
// PURITY (spec §3.1): Node built-ins + `src/api/*` only.
|
|
6
|
+
export {};
|
|
@@ -26,6 +26,11 @@ export interface SendMessageRequest {
|
|
|
26
26
|
* body only — rejected with fresh/reopen/situational_context/
|
|
27
27
|
* output_schema or tier 'deferred'. Absent → durable inbox (unchanged). */
|
|
28
28
|
delivery?: 'interactive';
|
|
29
|
+
/** Sender-minted message identity for a `delivery:'interactive'` send. The
|
|
30
|
+
* broker mirrors it through pi's queues and echoes it on the relayed user
|
|
31
|
+
* `message_start` (`crtrMessageId`), so an optimistic sender (the gateway)
|
|
32
|
+
* retires its pending entry by exact id. Ignored on the durable-inbox path. */
|
|
33
|
+
message_id?: string;
|
|
29
34
|
}
|
|
30
35
|
/** Result of an immediate message send. */
|
|
31
36
|
export interface MessageResultDTO {
|
|
@@ -38,6 +38,9 @@ export interface ReviewDTO {
|
|
|
38
38
|
created: IsoTime;
|
|
39
39
|
/** Daemon open timestamp, after the companion binds. */
|
|
40
40
|
opened_at?: IsoTime;
|
|
41
|
+
/** When the human submitted a review whose companion was still working. The
|
|
42
|
+
* review remains `open` until the companion goes quiet. */
|
|
43
|
+
submit_requested_at?: IsoTime;
|
|
41
44
|
/** Daemon source-read status at projection time. */
|
|
42
45
|
source_missing: boolean;
|
|
43
46
|
/** Daemon-owned current comment-coordinate source digest. */
|
|
@@ -73,12 +76,13 @@ export interface ListReviewsQuery {
|
|
|
73
76
|
export interface ReviewListDTO {
|
|
74
77
|
reviews: ReviewDTO[];
|
|
75
78
|
}
|
|
76
|
-
/** Daemon-derived result of
|
|
79
|
+
/** Daemon-derived result of review approval, which may still be waiting on the
|
|
80
|
+
* review's companion. */
|
|
77
81
|
export interface ReviewSubmitResultDTO {
|
|
78
82
|
review: ReviewDTO;
|
|
79
|
-
/** Immutable approval result. */
|
|
80
|
-
result
|
|
81
|
-
outcome: 'settled' | 'already_settled';
|
|
83
|
+
/** Immutable approval result; absent while the approval awaits the companion. */
|
|
84
|
+
result?: FeedbackResultDTO;
|
|
85
|
+
outcome: 'settled' | 'already_settled' | 'awaiting_companion';
|
|
82
86
|
}
|
|
83
87
|
/** Daemon-derived result of terminal review cancellation. */
|
|
84
88
|
export interface ReviewCancelResultDTO {
|
|
@@ -21,6 +21,7 @@ export * from './dto/canvas.js';
|
|
|
21
21
|
export * from './dto/worktree.js';
|
|
22
22
|
export * from './dto/human.js';
|
|
23
23
|
export * from './dto/files.js';
|
|
24
|
+
export * from './dto/memory.js';
|
|
24
25
|
export * from './dto/inbox.js';
|
|
25
26
|
export * from './dto/reviews.js';
|
|
26
27
|
export * from './dto/review-comments.js';
|
|
@@ -22,6 +22,7 @@ export * from './dto/canvas.js';
|
|
|
22
22
|
export * from './dto/worktree.js';
|
|
23
23
|
export * from './dto/human.js';
|
|
24
24
|
export * from './dto/files.js';
|
|
25
|
+
export * from './dto/memory.js';
|
|
25
26
|
export * from './dto/inbox.js';
|
|
26
27
|
export * from './dto/reviews.js';
|
|
27
28
|
export * from './dto/review-comments.js';
|
|
@@ -46,6 +46,7 @@ export declare const routes: {
|
|
|
46
46
|
readonly cronPause: (cronId: string) => string;
|
|
47
47
|
readonly cronResume: (cronId: string) => string;
|
|
48
48
|
readonly cronRun: (cronId: string) => string;
|
|
49
|
+
readonly cronsPoke: () => string;
|
|
49
50
|
readonly canvasAttention: () => string;
|
|
50
51
|
readonly canvasAttentionCounts: () => string;
|
|
51
52
|
readonly canvasHistorySearch: () => string;
|
|
@@ -81,4 +82,5 @@ export declare const routes: {
|
|
|
81
82
|
readonly modelAuths: () => string;
|
|
82
83
|
readonly modelAuth: (provider: string) => string;
|
|
83
84
|
readonly filePeek: () => string;
|
|
85
|
+
readonly memoryResolve: () => string;
|
|
84
86
|
};
|
|
@@ -67,6 +67,7 @@ export const routes = {
|
|
|
67
67
|
cronPause: (cronId) => `${V}/crons/${cronId}/pause`,
|
|
68
68
|
cronResume: (cronId) => `${V}/crons/${cronId}/resume`,
|
|
69
69
|
cronRun: (cronId) => `${V}/crons/${cronId}/run`,
|
|
70
|
+
cronsPoke: () => `${V}/crons/poke`,
|
|
70
71
|
// Canvas maintenance / reads
|
|
71
72
|
canvasAttention: () => `${V}/canvas/attention`,
|
|
72
73
|
canvasAttentionCounts: () => `${V}/canvas/attention/counts`,
|
|
@@ -112,4 +113,7 @@ export const routes = {
|
|
|
112
113
|
// Host file read (browser file-peek panel). The absolute path rides as a
|
|
113
114
|
// `path` query param, not a path segment — it is not a single safe segment.
|
|
114
115
|
filePeek: () => `${V}/files/peek`,
|
|
116
|
+
// Memory-document resolution (a `[[name]]` link in a node's transcript). Both
|
|
117
|
+
// the name and the node it is resolved for ride as query params.
|
|
118
|
+
memoryResolve: () => `${V}/memory/resolve`,
|
|
115
119
|
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** One `[[name]]` link occurrence found in a document body. */
|
|
2
|
+
export interface DocLink {
|
|
3
|
+
/** Character index of the first `[` of `[[`. */
|
|
4
|
+
start: number;
|
|
5
|
+
/** Character index just past the closing `]]`. */
|
|
6
|
+
end: number;
|
|
7
|
+
/** The canonical document name between the brackets. */
|
|
8
|
+
name: string;
|
|
9
|
+
}
|
|
10
|
+
/** Is `name` a well-formed canonical doc-link name? */
|
|
11
|
+
export declare function isDocLinkName(name: string): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Find every `[[name]]` doc link in a markdown body, in order of appearance,
|
|
14
|
+
* skipping fenced code blocks and inline code spans (a link mentioned inside
|
|
15
|
+
* code is an example, not a reference). Occurrences are NOT deduplicated —
|
|
16
|
+
* callers collapse as needed.
|
|
17
|
+
*/
|
|
18
|
+
export declare function findDocLinks(body: string): DocLink[];
|
|
19
|
+
/** The distinct link names in a body, first-appearance order. */
|
|
20
|
+
export declare function docLinkNames(body: string): string[];
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// doc-link-grammar.ts — the single source of truth for the `[[canonical/name]]`
|
|
2
|
+
// memory-document link grammar. Zero imports, pure string ops, browser-safe —
|
|
3
|
+
// any surface that highlights, resolves, or lints doc links imports THIS module
|
|
4
|
+
// rather than re-implementing the bracket scan or the name shape. It ships to
|
|
5
|
+
// external consumers as `@north-light/crouter-api/doc-link-grammar`, so a
|
|
6
|
+
// browser UI rendering a node's transcript scans with the same grammar crtrd
|
|
7
|
+
// validates `/v1/memory/resolve` names with.
|
|
8
|
+
//
|
|
9
|
+
// A doc link is a durable cross-reference written INSIDE a memory document's
|
|
10
|
+
// body, pointing at another memory document by its exact canonical name (the
|
|
11
|
+
// same identifier `crtr memory read <name>` takes; a directory INDEX is linked
|
|
12
|
+
// by its folded bare-directory name). It is distinct from the interactive
|
|
13
|
+
// `/name` inline-reference grammar (`inline-ref-grammar.ts`), which scans a
|
|
14
|
+
// human's live composer text — links live in stored markdown and survive there.
|
|
15
|
+
//
|
|
16
|
+
// Links are POINTERS, never transclusion: nothing in the runtime auto-loads a
|
|
17
|
+
// linked body into context. `crtr memory lint` fails a link that resolves to no
|
|
18
|
+
// document; `crtr memory read` surfaces the resolved links of the doc it loads.
|
|
19
|
+
//
|
|
20
|
+
// This module never resolves anything: `findDocLinks` returns candidate links;
|
|
21
|
+
// a caller decides a link is live by testing `name` against the corpus of
|
|
22
|
+
// resolvable canonical names.
|
|
23
|
+
// A canonical doc name: one or more `[A-Za-z0-9_-]+` segments joined by `/`.
|
|
24
|
+
// Anything else between `[[` `]]` (spaces, `|` aliases, empty segments) is NOT
|
|
25
|
+
// a doc link — the grammar has no alias/label form, by design.
|
|
26
|
+
const NAME_RE = /^[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)*$/;
|
|
27
|
+
const LINK_RE = /\[\[([^\[\]\n]+)\]\]/g;
|
|
28
|
+
/** Is `name` a well-formed canonical doc-link name? */
|
|
29
|
+
export function isDocLinkName(name) {
|
|
30
|
+
return NAME_RE.test(name);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Find every `[[name]]` doc link in a markdown body, in order of appearance,
|
|
34
|
+
* skipping fenced code blocks and inline code spans (a link mentioned inside
|
|
35
|
+
* code is an example, not a reference). Occurrences are NOT deduplicated —
|
|
36
|
+
* callers collapse as needed.
|
|
37
|
+
*/
|
|
38
|
+
export function findDocLinks(body) {
|
|
39
|
+
const links = [];
|
|
40
|
+
for (const segment of proseSegments(body)) {
|
|
41
|
+
LINK_RE.lastIndex = 0;
|
|
42
|
+
let m;
|
|
43
|
+
while ((m = LINK_RE.exec(segment.text)) !== null) {
|
|
44
|
+
const name = m[1];
|
|
45
|
+
if (!isDocLinkName(name))
|
|
46
|
+
continue;
|
|
47
|
+
links.push({ start: segment.offset + m.index, end: segment.offset + m.index + m[0].length, name });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return links;
|
|
51
|
+
}
|
|
52
|
+
/** The distinct link names in a body, first-appearance order. */
|
|
53
|
+
export function docLinkNames(body) {
|
|
54
|
+
const seen = new Set();
|
|
55
|
+
const out = [];
|
|
56
|
+
for (const link of findDocLinks(body)) {
|
|
57
|
+
if (seen.has(link.name))
|
|
58
|
+
continue;
|
|
59
|
+
seen.add(link.name);
|
|
60
|
+
out.push(link.name);
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
const FENCE_RE = /^ {0,3}(`{3,}|~{3,})/;
|
|
65
|
+
function* proseSegments(body) {
|
|
66
|
+
let offset = 0;
|
|
67
|
+
let fence = null; // the opening fence marker while inside a block
|
|
68
|
+
for (const line of body.split('\n')) {
|
|
69
|
+
const lineLen = line.length + 1; // + '\n' (harmless overcount on last line)
|
|
70
|
+
const fenceMatch = line.match(FENCE_RE);
|
|
71
|
+
if (fence !== null) {
|
|
72
|
+
// Inside a fenced block: only a closing fence of the same char and at
|
|
73
|
+
// least the same length ends it. Nothing in here is prose.
|
|
74
|
+
if (fenceMatch && fenceMatch[1][0] === fence[0] && fenceMatch[1].length >= fence.length) {
|
|
75
|
+
fence = null;
|
|
76
|
+
}
|
|
77
|
+
offset += lineLen;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (fenceMatch) {
|
|
81
|
+
fence = fenceMatch[1];
|
|
82
|
+
offset += lineLen;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
// Prose line: split around inline code spans (single-backtick pairs).
|
|
86
|
+
yield* proseParts(line, offset);
|
|
87
|
+
offset += lineLen;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function* proseParts(line, offset) {
|
|
91
|
+
let i = 0;
|
|
92
|
+
while (i < line.length) {
|
|
93
|
+
const open = line.indexOf('`', i);
|
|
94
|
+
if (open === -1) {
|
|
95
|
+
yield { offset: offset + i, text: line.slice(i) };
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
// Backtick run: an inline span is delimited by runs of equal length.
|
|
99
|
+
let runEnd = open;
|
|
100
|
+
while (runEnd < line.length && line[runEnd] === '`')
|
|
101
|
+
runEnd++;
|
|
102
|
+
const run = line.slice(open, runEnd);
|
|
103
|
+
const close = line.indexOf(run, runEnd);
|
|
104
|
+
if (close === -1) {
|
|
105
|
+
// Unbalanced backticks: treat the rest as prose past the run.
|
|
106
|
+
yield { offset: offset + i, text: line.slice(i, open) };
|
|
107
|
+
i = runEnd;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
yield { offset: offset + i, text: line.slice(i, open) };
|
|
111
|
+
i = close + run.length;
|
|
112
|
+
}
|
|
113
|
+
}
|
package/package.json
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/crouter-api",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.181",
|
|
4
4
|
"description": "Typed crtrd /v1 API contract — DTOs, route builders, the error contract, and the CrtrClient. Zero runtime dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "./dist/index.js",
|
|
7
|
-
"types": "./dist/index.d.ts",
|
|
6
|
+
"main": "./dist/api/index.js",
|
|
7
|
+
"types": "./dist/api/index.d.ts",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
|
-
"types": "./dist/index.d.ts",
|
|
11
|
-
"import": "./dist/index.js",
|
|
12
|
-
"require": "./dist/index.js",
|
|
13
|
-
"default": "./dist/index.js"
|
|
10
|
+
"types": "./dist/api/index.d.ts",
|
|
11
|
+
"import": "./dist/api/index.js",
|
|
12
|
+
"require": "./dist/api/index.js",
|
|
13
|
+
"default": "./dist/api/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./doc-link-grammar": {
|
|
16
|
+
"types": "./dist/core/memory/doc-link-grammar.d.ts",
|
|
17
|
+
"import": "./dist/core/memory/doc-link-grammar.js",
|
|
18
|
+
"require": "./dist/core/memory/doc-link-grammar.js",
|
|
19
|
+
"default": "./dist/core/memory/doc-link-grammar.js"
|
|
14
20
|
}
|
|
15
21
|
},
|
|
16
22
|
"files": [
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|