@oxygen-agent/cli 1.1011.326 → 1.1011.440
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 +1 -1
- package/dist/command-manifest.js +15 -4
- package/dist/connectors-commands.d.ts +4 -1
- package/dist/connectors-commands.js +84 -10
- package/dist/contract-flags.d.ts +2 -0
- package/dist/contract-flags.js +26 -11
- package/dist/crm-support-flags.d.ts +11 -0
- package/dist/crm-support-flags.js +60 -0
- package/dist/customer-operation-commands.d.ts +5 -0
- package/dist/customer-operation-commands.js +101 -0
- package/dist/database-query-flags.d.ts +2 -0
- package/dist/database-query-flags.js +6 -0
- package/dist/help.d.ts +3 -1
- package/dist/help.js +43 -0
- package/dist/http-client.d.ts +2 -0
- package/dist/http-client.js +41 -1
- package/dist/index.js +640 -234
- package/dist/knowledge-repository-commands.js +12 -14
- package/dist/mailbox-ramp-notice.d.ts +11 -0
- package/dist/mailbox-ramp-notice.js +53 -0
- package/dist/settings-commands.d.ts +11 -0
- package/dist/settings-commands.js +73 -0
- package/dist/skills.d.ts +5 -4
- package/dist/skills.js +106 -79
- package/dist/tool-catalog-detail-command.d.ts +7 -0
- package/dist/tool-catalog-detail-command.js +19 -0
- package/dist/tool-catalog-search-window.d.ts +6 -0
- package/dist/tool-catalog-search-window.js +12 -0
- package/dist/transcript.js +1 -0
- package/node_modules/@oxygen/shared/dist/bounded-read-result.d.ts +4 -0
- package/node_modules/@oxygen/shared/dist/bounded-read-result.js +25 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +47 -1
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +625 -39
- package/node_modules/@oxygen/shared/dist/connection-approval.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/connection-approval.js +2 -0
- package/node_modules/@oxygen/shared/dist/contracts/browser-operations.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +110 -0
- package/node_modules/@oxygen/shared/dist/contracts/connectors.d.ts +32 -6
- package/node_modules/@oxygen/shared/dist/contracts/connectors.js +56 -4
- package/node_modules/@oxygen/shared/dist/contracts/crm-attributes.d.ts +4 -0
- package/node_modules/@oxygen/shared/dist/contracts/crm-attributes.js +39 -0
- package/node_modules/@oxygen/shared/dist/contracts/crm-enrichment-read.d.ts +21 -0
- package/node_modules/@oxygen/shared/dist/contracts/crm-enrichment-read.js +15 -0
- package/node_modules/@oxygen/shared/dist/contracts/crm-support.d.ts +117 -0
- package/node_modules/@oxygen/shared/dist/contracts/crm-support.js +43 -0
- package/node_modules/@oxygen/shared/dist/contracts/customer-operations.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +131 -0
- package/node_modules/@oxygen/shared/dist/contracts/dashboard-tabs.d.ts +3 -0
- package/node_modules/@oxygen/shared/dist/contracts/dashboard-tabs.js +2 -0
- package/node_modules/@oxygen/shared/dist/contracts/database.d.ts +30 -0
- package/node_modules/@oxygen/shared/dist/contracts/database.js +29 -0
- package/node_modules/@oxygen/shared/dist/contracts/integration-connect.d.ts +4 -0
- package/node_modules/@oxygen/shared/dist/contracts/integration-connect.js +7 -0
- package/node_modules/@oxygen/shared/dist/contracts/knowledge-content-reads.d.ts +43 -0
- package/node_modules/@oxygen/shared/dist/contracts/knowledge-content-reads.js +20 -0
- package/node_modules/@oxygen/shared/dist/contracts/linkedin-invites-list.d.ts +12 -0
- package/node_modules/@oxygen/shared/dist/contracts/linkedin-invites-list.js +8 -0
- package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +48 -2
- package/node_modules/@oxygen/shared/dist/contracts/operations.js +68 -2
- package/node_modules/@oxygen/shared/dist/contracts/table-grid-query.d.ts +4 -0
- package/node_modules/@oxygen/shared/dist/contracts/table-grid-query.js +6 -0
- package/node_modules/@oxygen/shared/dist/contracts/table-query-grid.d.ts +21 -0
- package/node_modules/@oxygen/shared/dist/contracts/table-query-grid.js +27 -0
- package/node_modules/@oxygen/shared/dist/contracts/tool-catalog-detail.d.ts +19 -0
- package/node_modules/@oxygen/shared/dist/contracts/tool-catalog-detail.js +10 -0
- package/node_modules/@oxygen/shared/dist/contracts/tool-catalog-search-window.d.ts +12 -0
- package/node_modules/@oxygen/shared/dist/contracts/tool-catalog-search-window.js +7 -0
- package/node_modules/@oxygen/shared/dist/contracts/workspace-pages.d.ts +17 -0
- package/node_modules/@oxygen/shared/dist/contracts/workspace-pages.js +49 -0
- package/node_modules/@oxygen/shared/dist/decision-ledger-parser.d.ts +32 -0
- package/node_modules/@oxygen/shared/dist/decision-ledger-parser.js +303 -0
- package/node_modules/@oxygen/shared/dist/decision-ledger.d.ts +10 -0
- package/node_modules/@oxygen/shared/dist/decision-ledger.generated.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/decision-ledger.generated.js +717 -0
- package/node_modules/@oxygen/shared/dist/decision-ledger.js +20 -0
- package/node_modules/@oxygen/shared/dist/email-campaign-ramp.d.ts +104 -0
- package/node_modules/@oxygen/shared/dist/email-campaign-ramp.js +250 -0
- package/node_modules/@oxygen/shared/dist/mcp-event-contract.d.ts +10 -7
- package/node_modules/@oxygen/shared/dist/mcp-event-contract.js +11 -7
- package/node_modules/@oxygen/shared/dist/mcp-table-widget.d.ts +50 -0
- package/node_modules/@oxygen/shared/dist/mcp-table-widget.js +24 -0
- package/node_modules/@oxygen/shared/dist/plan-band.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/plan-band.js +12 -0
- package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +20 -0
- package/node_modules/@oxygen/shared/dist/plan-limits.js +41 -0
- package/node_modules/@oxygen/shared/dist/provider-input-value.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/provider-input-value.js +10 -0
- package/node_modules/@oxygen/shared/dist/sequences.d.ts +33 -4
- package/node_modules/@oxygen/shared/dist/sequences.js +48 -13
- package/node_modules/@oxygen/shared/dist/user-capability-routing.js +17 -0
- package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
- package/node_modules/@oxygen/shared/package.json +8 -6
- package/package.json +1 -1
- package/node_modules/@oxygen/shared/dist/copilot-skills.d.ts +0 -15
- package/node_modules/@oxygen/shared/dist/copilot-skills.generated.d.ts +0 -31
- package/node_modules/@oxygen/shared/dist/copilot-skills.generated.js +0 -41
- package/node_modules/@oxygen/shared/dist/copilot-skills.js +0 -6
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { arr, enumOf, int, obj, str } from "./schema.js";
|
|
2
|
+
/** Explicit metadata projections; omitted native HTTP/CLI options retain full detail. */
|
|
3
|
+
export const toolCatalogDetailQueryContract = obj({
|
|
4
|
+
view: enumOf(["summary", "pricing", "access", "guidance", "field_names", "native_fields", "examples", "input_schema", "output_schema", "full"], {
|
|
5
|
+
description: "Summary defaults on MCP; full defaults on HTTP/CLI. Schema views stay exact.",
|
|
6
|
+
}),
|
|
7
|
+
field_keys: arr(str({ minLength: 1, maxLength: 200 }), { minItems: 1, maxItems: 20, description: "Exact native field keys; field_names/native_fields only." }),
|
|
8
|
+
field_query: str({ minLength: 1, maxLength: 200, description: "Substring of native field keys; not with field_keys." }),
|
|
9
|
+
max_chars: int({ minimum: 2_000, maximum: 40_000, description: "Native receipt window; the calling surface may enforce a lower cap." }),
|
|
10
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Existing native filters plus an optional window; omission retains full HTTP/CLI defaults. */
|
|
2
|
+
export declare const toolCatalogSearchWindowContract: import("./schema.js").ContractObject<{
|
|
3
|
+
readonly max_chars: {
|
|
4
|
+
type: "integer";
|
|
5
|
+
description?: string;
|
|
6
|
+
minimum?: number;
|
|
7
|
+
maximum?: number;
|
|
8
|
+
"x-clamp"?: true;
|
|
9
|
+
};
|
|
10
|
+
readonly include_native: import("./schema.js").BooleanProperty;
|
|
11
|
+
readonly workflow_eligible: import("./schema.js").BooleanProperty;
|
|
12
|
+
}, never>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { bool, int, obj } from "./schema.js";
|
|
2
|
+
/** Existing native filters plus an optional window; omission retains full HTTP/CLI defaults. */
|
|
3
|
+
export const toolCatalogSearchWindowContract = obj({
|
|
4
|
+
max_chars: int({ minimum: 2_000, maximum: 40_000, description: "Native window; surface cap applies." }),
|
|
5
|
+
include_native: bool({ description: "Include native column metadata (default true)." }),
|
|
6
|
+
workflow_eligible: bool({ description: "Only hosted-workflow tools; includes canonical workflow_effect." }),
|
|
7
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `POST /api/cli/settings/pages`: the web app's sidebar pages a workspace hides
|
|
3
|
+
* for every member (Settings → Customise; Philipp, 2026-09-30). Presentation
|
|
4
|
+
* only: a hidden page leaves the sidebar and the ⌘K palette, and stays
|
|
5
|
+
* reachable by URL and deep-link. Admins and owners change it; every member
|
|
6
|
+
* reads it through `GET` on the same route.
|
|
7
|
+
*/
|
|
8
|
+
import type { OperationSpec } from "./operations.js";
|
|
9
|
+
import { type Infer } from "./schema.js";
|
|
10
|
+
export declare const WORKSPACE_PAGES_UPDATE_CONTRACT: import("./schema.js").ContractObject<{
|
|
11
|
+
readonly hide: import("./schema.js").ArrayProperty<import("./schema.js").StringProperty>;
|
|
12
|
+
readonly show: import("./schema.js").ArrayProperty<import("./schema.js").StringProperty>;
|
|
13
|
+
readonly show_all: import("./schema.js").BooleanProperty;
|
|
14
|
+
}, never>;
|
|
15
|
+
export type WorkspacePagesUpdate = Infer<typeof WORKSPACE_PAGES_UPDATE_CONTRACT>;
|
|
16
|
+
export declare const WORKSPACE_PAGES_GET_OPERATION: OperationSpec;
|
|
17
|
+
export declare const WORKSPACE_PAGES_UPDATE_OPERATION: OperationSpec<typeof WORKSPACE_PAGES_UPDATE_CONTRACT>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { arr, bool, obj, str } from "./schema.js";
|
|
2
|
+
const pageRef = str({ minLength: 1, maxLength: 200 });
|
|
3
|
+
export const WORKSPACE_PAGES_UPDATE_CONTRACT = obj({
|
|
4
|
+
hide: arr(pageRef, {
|
|
5
|
+
minItems: 1,
|
|
6
|
+
maxItems: 100,
|
|
7
|
+
description: "Pages to hide for everyone, each by its path (/unibox) or name (Unibox) from the page list. Hiding a section hides its sub-pages.",
|
|
8
|
+
}),
|
|
9
|
+
show: arr(pageRef, {
|
|
10
|
+
minItems: 1,
|
|
11
|
+
maxItems: 100,
|
|
12
|
+
description: "Hidden pages to show again, by path or name.",
|
|
13
|
+
}),
|
|
14
|
+
show_all: bool({
|
|
15
|
+
description: "Show every page again. With hide, only those pages stay hidden.",
|
|
16
|
+
}),
|
|
17
|
+
});
|
|
18
|
+
const inferMatchesRoute = true;
|
|
19
|
+
void inferMatchesRoute;
|
|
20
|
+
export const WORKSPACE_PAGES_GET_OPERATION = {
|
|
21
|
+
id: "workspace_pages.get",
|
|
22
|
+
route: "/api/cli/settings/pages",
|
|
23
|
+
method: "GET",
|
|
24
|
+
contract: obj({}),
|
|
25
|
+
generated: { cli: false, mcp: false },
|
|
26
|
+
cli: { path: ["settings", "pages", "list"], summary: "Page visibility in the web app's sidebar navigation menu (Settings → Customise): hidden_pages, whether you may customize it (can_change: admins and owners), then every page (path, name, section, parent) with hidden (hidden itself) and visible (shown in the sidebar; false under a hidden section). Read-only, 0 credits." },
|
|
27
|
+
mcp: { name: "oxygen_settings_pages_get", title: "Oxygen: List Sidebar Pages", description: "Lists the OXYGEN web app's sidebar navigation pages (Settings → Customise) with whether this workspace hides each one for everyone, and can_change (admins and owners). Free. Change with oxygen_settings_pages_update.", pack: "workspace" },
|
|
28
|
+
mutates: false,
|
|
29
|
+
spends: false,
|
|
30
|
+
};
|
|
31
|
+
export const WORKSPACE_PAGES_UPDATE_OPERATION = {
|
|
32
|
+
id: "workspace_pages.update",
|
|
33
|
+
route: "/api/cli/settings/pages",
|
|
34
|
+
method: "POST",
|
|
35
|
+
contract: WORKSPACE_PAGES_UPDATE_CONTRACT,
|
|
36
|
+
generated: { cli: false, mcp: false },
|
|
37
|
+
cli: {
|
|
38
|
+
path: ["settings", "pages", "set"],
|
|
39
|
+
summary: "Hide or show pages in the web app's sidebar navigation menu for everyone in this workspace (Settings → Customise): --hide Unibox,Publishing, --show Unibox, --show-all. Name pages by path or name from `settings pages list`; hiding a section hides its sub-pages. Nothing is deleted and hidden pages stay reachable by URL. Admins and owners only; free.",
|
|
40
|
+
},
|
|
41
|
+
mcp: {
|
|
42
|
+
name: "oxygen_settings_pages_update",
|
|
43
|
+
title: "Oxygen: Hide or Show Sidebar Pages",
|
|
44
|
+
description: "Hides or shows pages in the OXYGEN web app's sidebar navigation for everyone in this workspace (Settings → Customise). hide/show take page paths or names from oxygen_settings_pages_get; show_all shows every page again. Nothing is deleted and hidden pages stay reachable by URL. Admins and owners only. Free.",
|
|
45
|
+
pack: "workspace",
|
|
46
|
+
},
|
|
47
|
+
mutates: true,
|
|
48
|
+
spends: false,
|
|
49
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type DecisionLedgerStatus = "ratified" | "proposed" | "open";
|
|
2
|
+
export type DecisionLedgerEntry = {
|
|
3
|
+
/** Ledger id as written, e.g. "D24" or "D34a". */
|
|
4
|
+
id: string;
|
|
5
|
+
/** The "Decision" cell as plain text. */
|
|
6
|
+
title: string;
|
|
7
|
+
/** The answer as plain text: the ratified or proposed answer, or the Open table's "Status" cell. */
|
|
8
|
+
answer: string;
|
|
9
|
+
/** In force (`ratified`), awaiting the founder (`proposed`), or undecided (`open`). */
|
|
10
|
+
status: DecisionLedgerStatus;
|
|
11
|
+
/** The table the row sits in. A Proposed row ratified in chat keeps `proposed` here. */
|
|
12
|
+
table: "ratified" | "open" | "proposed";
|
|
13
|
+
/** The ISO date (YYYY-MM-DD) it was ratified, when the ledger records one. */
|
|
14
|
+
ratifiedOn: string | null;
|
|
15
|
+
/**
|
|
16
|
+
* Where it shipped or lands: the Ratified table's "Shipped / lands" cell, the
|
|
17
|
+
* Open table's "Owner / forcing function" cell, or what follows the
|
|
18
|
+
* ratification clause in a Proposed row's status cell.
|
|
19
|
+
*/
|
|
20
|
+
lands: string | null;
|
|
21
|
+
/** Decisions that replace this one outright, sorted by id. */
|
|
22
|
+
supersededBy: string[];
|
|
23
|
+
/**
|
|
24
|
+
* Decisions that replace only part of this one while the rest stands
|
|
25
|
+
* ("Supersedes only D19(a)'s … clause", "Supersedes D55's company half",
|
|
26
|
+
* "Superseded by D29 … as a routing posture"), sorted by id. A decision that
|
|
27
|
+
* another statement calls replaced outright is listed in `supersededBy` only.
|
|
28
|
+
*/
|
|
29
|
+
partlySupersededBy: string[];
|
|
30
|
+
};
|
|
31
|
+
/** Parse the ledger markdown into entries sorted by decision id. */
|
|
32
|
+
export declare function parseDecisionLedger(markdown: string): DecisionLedgerEntry[];
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
// Parser for the routing program's decision ledger (docs/gtm-program/decisions.md).
|
|
2
|
+
//
|
|
3
|
+
// Registry seeds cite ledger ids, and staff surfaces explain a provider choice
|
|
4
|
+
// by them, so the ledger is read as data rather than prose. The parser keys on
|
|
5
|
+
// the three table sections and their exact header rows and throws on anything
|
|
6
|
+
// else: a reshaped ledger has to break the generator, not lose decisions.
|
|
7
|
+
//
|
|
8
|
+
// This module imports nothing, not even a sibling, so
|
|
9
|
+
// scripts/docs/generate-decision-ledger.mjs can load it through Node's own type
|
|
10
|
+
// stripping without a build, and so it stays browser-safe. Consumers read the
|
|
11
|
+
// parsed entries through @oxygen/shared/decision-ledger.
|
|
12
|
+
const TABLE_LAYOUTS = [
|
|
13
|
+
{ table: "ratified", heading: /^Ratified$/, columns: ["#", "Decision", "Ratified answer", "Shipped / lands"] },
|
|
14
|
+
{ table: "open", heading: /^Open$/, columns: ["#", "Decision", "Status", "Owner / forcing function"] },
|
|
15
|
+
{ table: "proposed", heading: /^Proposed\b/, columns: ["#", "Decision", "Proposed answer", "Status"] },
|
|
16
|
+
];
|
|
17
|
+
const DECISION_ID = String.raw `D\d+[a-z]?(?:\([a-z]\))?`;
|
|
18
|
+
const DECISION_ID_LIST = String.raw `${DECISION_ID}(?:(?:\s*,\s*(?:and\s+|or\s+)?|\s*[/&]\s*|\s+(?:and|or)\s+)${DECISION_ID})*`;
|
|
19
|
+
// "Superseded by D26", "Superseded by D55/D56" — this row is replaced.
|
|
20
|
+
const SUPERSEDED_BY = new RegExp(String.raw `\b[Ss]uperseded\s+by\s+(${DECISION_ID_LIST})`, "g");
|
|
21
|
+
// "Supersedes D24(a)", "Supersedes only D19(a)'s clause", "Supersedes the D32 chain".
|
|
22
|
+
const SUPERSEDES = new RegExp(String.raw `\b[Ss]upersedes\s+(only\s+|the\s+)?(${DECISION_ID_LIST})`, "g");
|
|
23
|
+
// What follows a superseded id when only part of it is replaced: a possessive
|
|
24
|
+
// ("D55's company half"), a capacity ("D18(a) as a routing posture") or "in
|
|
25
|
+
// part", optionally after the date the row records ("D29 (2026-07-25) as a …").
|
|
26
|
+
const PARTIAL_SCOPE = /^\s*(?:\([^)]*\)\s*)?(?:['\u2019]s\b|as\s+an?\s|in\s+part\b)/;
|
|
27
|
+
// "Partially superseded by D40": the row itself says only part of it went.
|
|
28
|
+
const PARTIAL_PREFIX = /\b(?:[Pp]artially|[Pp]artly)\s+$/;
|
|
29
|
+
// A decision id with an optional attached letter (D34a) and an optional answer
|
|
30
|
+
// option in parentheses (D19(a)); the option names a clause, not a decision.
|
|
31
|
+
const ID_MENTION = /\bD(\d+)([a-z]?)(?:\([a-z]\))?(?![\w(])/g;
|
|
32
|
+
const ID_RANGE = /\bD(\d+)\s*[–—-]\s*D(\d+)\b/g;
|
|
33
|
+
/** Parse the ledger markdown into entries sorted by decision id. */
|
|
34
|
+
export function parseDecisionLedger(markdown) {
|
|
35
|
+
const lines = markdown.split(/\r?\n/);
|
|
36
|
+
const rows = [];
|
|
37
|
+
const events = [];
|
|
38
|
+
const tablesSeen = new Set();
|
|
39
|
+
let section = null;
|
|
40
|
+
let index = 0;
|
|
41
|
+
while (index < lines.length) {
|
|
42
|
+
const line = lines[index] ?? "";
|
|
43
|
+
const heading = /^##\s+(.+?)\s*#*\s*$/.exec(line);
|
|
44
|
+
if (heading) {
|
|
45
|
+
const title = plainText(heading[1] ?? "");
|
|
46
|
+
section = { title, layout: TABLE_LAYOUTS.find((layout) => layout.heading.test(title)) ?? null, hasTable: false };
|
|
47
|
+
index += 1;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (/^\s*\|/.test(line)) {
|
|
51
|
+
const where = section ? `"## ${section.title}"` : "the preamble";
|
|
52
|
+
if (!section?.layout) {
|
|
53
|
+
throw new Error(`Decision ledger line ${index + 1}: a table under ${where}. The parser reads only the Ratified, Open and Proposed tables and will not guess what this one holds; extend parseDecisionLedger first.`);
|
|
54
|
+
}
|
|
55
|
+
if (section.hasTable || tablesSeen.has(section.layout.table)) {
|
|
56
|
+
throw new Error(`Decision ledger line ${index + 1}: a second ${section.layout.table} table under ${where}.`);
|
|
57
|
+
}
|
|
58
|
+
section.hasTable = true;
|
|
59
|
+
tablesSeen.add(section.layout.table);
|
|
60
|
+
index = readTable(lines, index, section, section.layout, rows);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (section === null) {
|
|
64
|
+
// Preamble: "- **2026-06-10 (T0 build):** D7, D11, ...", possibly wrapped.
|
|
65
|
+
const event = /^\s*[-*+]\s+\*\*(\d{4}-\d{2}-\d{2})\b[^*]*\*\*(.*)$/.exec(line);
|
|
66
|
+
if (event) {
|
|
67
|
+
let text = event[2] ?? "";
|
|
68
|
+
while (index + 1 < lines.length && /^\s+\S/.test(lines[index + 1] ?? "") && !/^\s*[-*+]\s/.test(lines[index + 1] ?? "")) {
|
|
69
|
+
index += 1;
|
|
70
|
+
text += ` ${lines[index] ?? ""}`;
|
|
71
|
+
}
|
|
72
|
+
events.push({ date: event[1] ?? "", text });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
index += 1;
|
|
76
|
+
}
|
|
77
|
+
if (rows.length === 0)
|
|
78
|
+
throw new Error("Decision ledger has no Ratified, Open or Proposed table rows.");
|
|
79
|
+
return buildEntries(rows, events);
|
|
80
|
+
}
|
|
81
|
+
function readTable(lines, start, section, layout, rows) {
|
|
82
|
+
const header = splitRow(lines[start] ?? "").map(plainText);
|
|
83
|
+
if (header.join(" | ") !== layout.columns.join(" | ")) {
|
|
84
|
+
throw new Error(`The "## ${section.title}" table (line ${start + 1}) has columns "${header.join(" | ")}"; the parser expects "${layout.columns.join(" | ")}". Update parseDecisionLedger for the new layout rather than letting it guess.`);
|
|
85
|
+
}
|
|
86
|
+
const delimiter = splitRow(lines[start + 1] ?? "");
|
|
87
|
+
if (delimiter.length !== header.length || !delimiter.every((cell) => /^:?-{3,}:?$/.test(cell))) {
|
|
88
|
+
throw new Error(`The "## ${section.title}" table (line ${start + 2}) has no delimiter row under its header.`);
|
|
89
|
+
}
|
|
90
|
+
let index = start + 2;
|
|
91
|
+
while (index < lines.length && /^\s*\|/.test(lines[index] ?? "")) {
|
|
92
|
+
const cells = splitRow(lines[index] ?? "");
|
|
93
|
+
if (cells.length !== header.length) {
|
|
94
|
+
throw new Error(`Decision ledger row ${plainText(cells[0] ?? "") || "?"} in the ${layout.table} table (line ${index + 1}) has ${cells.length} cells; its header has ${header.length}.`);
|
|
95
|
+
}
|
|
96
|
+
rows.push({ table: layout.table, cells, line: index + 1 });
|
|
97
|
+
index += 1;
|
|
98
|
+
}
|
|
99
|
+
return index;
|
|
100
|
+
}
|
|
101
|
+
/** Split a GFM table row on unescaped pipes; `\|` stays escaped for plainText. */
|
|
102
|
+
function splitRow(line) {
|
|
103
|
+
let body = line.trim();
|
|
104
|
+
if (body.startsWith("|"))
|
|
105
|
+
body = body.slice(1);
|
|
106
|
+
if (body.endsWith("|") && !body.endsWith("\\|"))
|
|
107
|
+
body = body.slice(0, -1);
|
|
108
|
+
const cells = [];
|
|
109
|
+
let current = "";
|
|
110
|
+
for (let position = 0; position < body.length; position += 1) {
|
|
111
|
+
const char = body[position];
|
|
112
|
+
if (char === "\\" && body[position + 1] === "|") {
|
|
113
|
+
current += "\\|";
|
|
114
|
+
position += 1;
|
|
115
|
+
}
|
|
116
|
+
else if (char === "|") {
|
|
117
|
+
cells.push(current.trim());
|
|
118
|
+
current = "";
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
current += char;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
cells.push(current.trim());
|
|
125
|
+
return cells;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Markdown inline text to plain text: code spans keep their content verbatim,
|
|
129
|
+
* emphasis and strikethrough markers go, links keep their text, escapes resolve,
|
|
130
|
+
* whitespace collapses. Intraword underscores (find_email) are never emphasis.
|
|
131
|
+
*/
|
|
132
|
+
function plainText(markdown) {
|
|
133
|
+
const held = [];
|
|
134
|
+
const hold = (value) => `\u0000${held.push(value) - 1}\u0000`;
|
|
135
|
+
let text = markdown
|
|
136
|
+
// GFM resolves `\|` inside a table cell before any inline parsing, code spans included.
|
|
137
|
+
.replace(/\\\|/g, "|")
|
|
138
|
+
.replace(/(`+)([\s\S]*?[^`])\1(?!`)/g, (_match, _ticks, code) => hold(code.length > 2 && code.startsWith(" ") && code.endsWith(" ") ? code.slice(1, -1) : code))
|
|
139
|
+
.replace(/\\([!-/:-@[-`{-~])/g, (_match, char) => hold(char))
|
|
140
|
+
.replace(/!?\[([^\]]*)\]\([^)]*\)/g, "$1")
|
|
141
|
+
.replace(/<((?:https?|mailto):[^>\s]+)>/g, "$1");
|
|
142
|
+
for (;;) {
|
|
143
|
+
const next = text
|
|
144
|
+
.replace(/\*\*(?=\S)([\s\S]*?\S)\*\*/g, "$1")
|
|
145
|
+
.replace(/(^|\W)__(?=\S)([\s\S]*?\S)__(?!\w)/g, "$1$2")
|
|
146
|
+
.replace(/(^|[^\w*])\*(?=[^\s*])([\s\S]*?[^\s*])\*(?![\w*])/g, "$1$2")
|
|
147
|
+
.replace(/(^|\W)_(?=[^\s_])([\s\S]*?[^\s_])_(?!\w)/g, "$1$2")
|
|
148
|
+
.replace(/~~(?=\S)([\s\S]*?\S)~~/g, "$1");
|
|
149
|
+
if (next === text)
|
|
150
|
+
break;
|
|
151
|
+
text = next;
|
|
152
|
+
}
|
|
153
|
+
return text
|
|
154
|
+
.replace(/\u0000(\d+)\u0000/g, (_match, slot) => held[Number(slot)] ?? "")
|
|
155
|
+
.replace(/\s+/g, " ")
|
|
156
|
+
.trim();
|
|
157
|
+
}
|
|
158
|
+
/** Sentences of plain text; a period followed by a lowercase word ("i.e. we") does not split. */
|
|
159
|
+
function sentences(text) {
|
|
160
|
+
return text.split(/(?<=[.!?])\s+(?=[A-Z0-9("'`])/);
|
|
161
|
+
}
|
|
162
|
+
/** Decision ids mentioned in text, as `D<n>` plus any attached letter; "D19(a)" is "D19". */
|
|
163
|
+
function mentionedIds(text, options = {}) {
|
|
164
|
+
const ids = [];
|
|
165
|
+
if (options.expandRanges) {
|
|
166
|
+
for (const range of text.matchAll(ID_RANGE)) {
|
|
167
|
+
const from = Number(range[1]);
|
|
168
|
+
const to = Number(range[2]);
|
|
169
|
+
if (to < from || to - from > 500)
|
|
170
|
+
continue;
|
|
171
|
+
for (let number = from; number <= to; number += 1)
|
|
172
|
+
ids.push(`D${number}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
for (const mention of text.matchAll(ID_MENTION))
|
|
176
|
+
ids.push(`D${mention[1]}${mention[2] ?? ""}`);
|
|
177
|
+
return ids;
|
|
178
|
+
}
|
|
179
|
+
/** Exact id first ("D34a"), then the base id for an option-lettered mention ("D19a" -> "D19"). */
|
|
180
|
+
function resolveId(mention, known) {
|
|
181
|
+
if (known.has(mention))
|
|
182
|
+
return mention;
|
|
183
|
+
const base = /^(D\d+)[a-z]$/.exec(mention)?.[1];
|
|
184
|
+
return base && known.has(base) ? base : null;
|
|
185
|
+
}
|
|
186
|
+
function compareIds(left, right) {
|
|
187
|
+
const a = /^D(\d+)([a-z]?)$/.exec(left);
|
|
188
|
+
const b = /^D(\d+)([a-z]?)$/.exec(right);
|
|
189
|
+
const byNumber = Number(a?.[1] ?? 0) - Number(b?.[1] ?? 0);
|
|
190
|
+
if (byNumber !== 0)
|
|
191
|
+
return byNumber;
|
|
192
|
+
return (a?.[2] ?? "").localeCompare(b?.[2] ?? "");
|
|
193
|
+
}
|
|
194
|
+
function nullIfEmpty(text) {
|
|
195
|
+
return text.length > 0 ? text : null;
|
|
196
|
+
}
|
|
197
|
+
/** The status cell of a Proposed-table row: "Proposed" or "Ratified in chat <date> (<who>); <lands>". */
|
|
198
|
+
function readProposedStatus(id, line, cell) {
|
|
199
|
+
const ratified = /^Ratified in chat (\d{4}-\d{2}-\d{2})\b(?:\s*\([^)]*\))?\s*(?:[;,.:—–-]\s*)?(.*)$/i.exec(cell);
|
|
200
|
+
if (ratified)
|
|
201
|
+
return { status: "ratified", ratifiedOn: ratified[1] ?? null, lands: nullIfEmpty(ratified[2] ?? "") };
|
|
202
|
+
const proposed = /^Proposed\b\s*(?:[;,.:—–-]\s*)?(.*)$/i.exec(cell);
|
|
203
|
+
if (proposed)
|
|
204
|
+
return { status: "proposed", ratifiedOn: null, lands: nullIfEmpty(proposed[1] ?? "") };
|
|
205
|
+
throw new Error(`Decision ${id} (line ${line}) has a Proposed-table status the parser does not recognise: "${cell}". Expected "Proposed" or "Ratified in chat YYYY-MM-DD".`);
|
|
206
|
+
}
|
|
207
|
+
function buildEntries(rows, events) {
|
|
208
|
+
const entries = [];
|
|
209
|
+
const lineById = new Map();
|
|
210
|
+
const searchText = new Map();
|
|
211
|
+
for (const row of rows) {
|
|
212
|
+
const [idCell = "", titleCell = "", answerCell = "", lastCell = ""] = row.cells.map(plainText);
|
|
213
|
+
if (!/^D\d+[a-z]?$/.test(idCell)) {
|
|
214
|
+
throw new Error(`Decision ledger line ${row.line}: "${idCell}" is not a decision id (D<number>, optionally one letter).`);
|
|
215
|
+
}
|
|
216
|
+
const firstLine = lineById.get(idCell);
|
|
217
|
+
if (firstLine !== undefined) {
|
|
218
|
+
throw new Error(`Decision ${idCell} appears more than once in the ledger (lines ${firstLine} and ${row.line}); ids are never reused.`);
|
|
219
|
+
}
|
|
220
|
+
lineById.set(idCell, row.line);
|
|
221
|
+
const base = {
|
|
222
|
+
id: idCell,
|
|
223
|
+
title: titleCell,
|
|
224
|
+
answer: answerCell,
|
|
225
|
+
table: row.table,
|
|
226
|
+
supersededBy: [],
|
|
227
|
+
partlySupersededBy: [],
|
|
228
|
+
};
|
|
229
|
+
if (row.table === "proposed") {
|
|
230
|
+
entries.push({ ...base, ...readProposedStatus(idCell, row.line, lastCell) });
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
entries.push({ ...base, status: row.table, ratifiedOn: null, lands: nullIfEmpty(lastCell) });
|
|
234
|
+
}
|
|
235
|
+
searchText.set(idCell, [titleCell, answerCell, lastCell].join(" "));
|
|
236
|
+
}
|
|
237
|
+
const known = new Set(entries.map((entry) => entry.id));
|
|
238
|
+
// Ratification events date the Ratified table. A sentence that records a
|
|
239
|
+
// supersession ("D15 ... superseded in place") is not a ratification.
|
|
240
|
+
const eventDates = new Map();
|
|
241
|
+
for (const event of events) {
|
|
242
|
+
for (const sentence of sentences(plainText(event.text))) {
|
|
243
|
+
if (/supersed/i.test(sentence))
|
|
244
|
+
continue;
|
|
245
|
+
for (const mention of mentionedIds(sentence, { expandRanges: true })) {
|
|
246
|
+
const id = resolveId(mention, known);
|
|
247
|
+
if (!id)
|
|
248
|
+
continue;
|
|
249
|
+
const current = eventDates.get(id);
|
|
250
|
+
if (current === undefined || event.date < current)
|
|
251
|
+
eventDates.set(id, event.date);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
for (const entry of entries) {
|
|
256
|
+
if (entry.table !== "ratified")
|
|
257
|
+
continue;
|
|
258
|
+
// Otherwise the row's own clause: "Ratified 2026-07-25 (moved from Open)", "Closed 2026-07-25".
|
|
259
|
+
const rowDate = /\b(?:Ratified|Closed)\b[^.;:]*?(\d{4}-\d{2}-\d{2})/.exec(entry.answer)?.[1];
|
|
260
|
+
entry.ratifiedOn = eventDates.get(entry.id) ?? rowDate ?? null;
|
|
261
|
+
}
|
|
262
|
+
// Supersession, both directions: "Superseded by D26" on the row itself, and
|
|
263
|
+
// "Supersedes D24(a)" on the row that replaced it. A statement that names a
|
|
264
|
+
// part ("only", a possessive, "as a …") replaces only that part.
|
|
265
|
+
const replaced = { whole: new Map(), partly: new Map() };
|
|
266
|
+
const record = (superseded, by, partly) => {
|
|
267
|
+
if (superseded === by)
|
|
268
|
+
return;
|
|
269
|
+
const target = partly ? replaced.partly : replaced.whole;
|
|
270
|
+
const set = target.get(superseded) ?? new Set();
|
|
271
|
+
set.add(by);
|
|
272
|
+
target.set(superseded, set);
|
|
273
|
+
};
|
|
274
|
+
const resolveOrThrow = (mention, owner, verb) => {
|
|
275
|
+
const id = resolveId(mention, known);
|
|
276
|
+
if (!id)
|
|
277
|
+
throw new Error(`Decision ${owner} says it ${verb} ${mention}, which the ledger does not define.`);
|
|
278
|
+
return id;
|
|
279
|
+
};
|
|
280
|
+
const after = (text, match) => text.slice((match.index ?? 0) + match[0].length);
|
|
281
|
+
for (const entry of entries) {
|
|
282
|
+
const text = searchText.get(entry.id) ?? "";
|
|
283
|
+
for (const match of text.matchAll(SUPERSEDED_BY)) {
|
|
284
|
+
const partly = PARTIAL_PREFIX.test(text.slice(0, match.index ?? 0)) || PARTIAL_SCOPE.test(after(text, match));
|
|
285
|
+
for (const mention of mentionedIds(match[1] ?? "")) {
|
|
286
|
+
record(entry.id, resolveOrThrow(mention, entry.id, "is superseded by"), partly);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
for (const match of text.matchAll(SUPERSEDES)) {
|
|
290
|
+
const partly = (match[1] ?? "").trim() === "only" || PARTIAL_SCOPE.test(after(text, match));
|
|
291
|
+
for (const mention of mentionedIds(match[2] ?? "")) {
|
|
292
|
+
record(resolveOrThrow(mention, entry.id, "supersedes"), entry.id, partly);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
for (const entry of entries) {
|
|
297
|
+
const whole = replaced.whole.get(entry.id) ?? new Set();
|
|
298
|
+
entry.supersededBy = [...whole].sort(compareIds);
|
|
299
|
+
// One unqualified statement settles it: a decision a row calls replaced is replaced.
|
|
300
|
+
entry.partlySupersededBy = [...(replaced.partly.get(entry.id) ?? [])].filter((id) => !whole.has(id)).sort(compareIds);
|
|
301
|
+
}
|
|
302
|
+
return entries.sort((left, right) => compareIds(left.id, right.id));
|
|
303
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { DecisionLedgerEntry } from "./decision-ledger-parser.js";
|
|
2
|
+
export { parseDecisionLedger, type DecisionLedgerEntry, type DecisionLedgerStatus } from "./decision-ledger-parser.js";
|
|
3
|
+
/**
|
|
4
|
+
* The ledger entry for a decision id. An exact id wins ("D34a" is its own
|
|
5
|
+
* decision); otherwise an answer-option suffix resolves to its base decision
|
|
6
|
+
* ("D19a" and "D19(a)" are D19's option (a)).
|
|
7
|
+
*/
|
|
8
|
+
export declare function getDecision(id: string): DecisionLedgerEntry | null;
|
|
9
|
+
/** Every ledger entry, sorted by decision id. */
|
|
10
|
+
export declare function listDecisions(): readonly DecisionLedgerEntry[];
|