@augurworks/augur 0.15.7 → 0.15.9
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 +21 -0
- package/package.json +1 -1
- package/scripts/connect.mjs +43 -1
- package/scripts/lib/adapters.mjs +6 -1
- package/scripts/open.mjs +8 -2
- package/src/_worker.js +58 -17
- package/src/chrome/appchrome.mjs +8 -2
package/README.md
CHANGED
|
@@ -53,6 +53,27 @@ contracts in [agents/](./agents/) does the same job. Screens in the demo space
|
|
|
53
53
|
carry the session that made them: [open the repo and read one](https://github.com/andratwiro/augur-space-fulla/tree/main/garden/prompts).
|
|
54
54
|
The follow-ups read like design direction, because that is what they are.
|
|
55
55
|
|
|
56
|
+
## Let an assistant in from a link
|
|
57
|
+
|
|
58
|
+
Someone on the team gives their assistant the workspace address and asks for a
|
|
59
|
+
change. Nothing else is needed from them, and no password ever changes hands.
|
|
60
|
+
The workspace tells the assistant how to get in (`/llms.txt` on every instance),
|
|
61
|
+
and the flow is device pairing:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npx @augurworks/augur connect --origin https://<the workspace>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
It prints a link and a code. The person opens the link in the browser they are
|
|
68
|
+
already signed in to and types the code; that pairs the one terminal that printed
|
|
69
|
+
it, to publish as them. From then on the assistant opens a prototype as a draft
|
|
70
|
+
(`augur open <opportunity>/<prototype>`), edits it, live at the draft's own
|
|
71
|
+
address, and lands it (`augur land`), which moves the real URL. Signed-in members
|
|
72
|
+
see the same instructions under Help › Building and at `/__connect`, so a person
|
|
73
|
+
who knows nothing technical can still confirm what their assistant was told. The
|
|
74
|
+
package on npm is this repository, and nothing else: `npm view @augurworks/augur
|
|
75
|
+
repository`.
|
|
76
|
+
|
|
56
77
|
## Boards where the prototypes run
|
|
57
78
|
|
|
58
79
|
Drop a live prototype next to the stickies and drive it. Everyone on the board
|
package/package.json
CHANGED
package/scripts/connect.mjs
CHANGED
|
@@ -19,6 +19,7 @@ import path from "node:path";
|
|
|
19
19
|
import os from "node:os";
|
|
20
20
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
21
21
|
import { resolveOrigin } from "./lib/store.mjs";
|
|
22
|
+
import { augurOnPath } from "./lib/adapters.mjs";
|
|
22
23
|
|
|
23
24
|
const C = { dim: "\x1b[2m", bold: "\x1b[1m", ok: "\x1b[32m", warn: "\x1b[33m", off: "\x1b[0m" };
|
|
24
25
|
const log = (m) => console.log(`\x1b[35m[connect]\x1b[0m ${m}`);
|
|
@@ -64,6 +65,37 @@ async function post(pathPart, body) {
|
|
|
64
65
|
return { status: r.status, json };
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
// A machine that is ALREADY connected to this origin does not pair again by accident. One
|
|
69
|
+
// cold agent collected its token with a blocking `connect`, did not read the answer, ran
|
|
70
|
+
// `connect` again, minted a second code, and had the person approve that too. If the saved
|
|
71
|
+
// token still answers, say so and stop; `--again` pairs afresh on purpose, and a token the
|
|
72
|
+
// workspace no longer honours (revoked, another workspace's) falls through to a new pairing.
|
|
73
|
+
const AGAIN = argv.includes("--again");
|
|
74
|
+
// Whether this workspace serves drafts, from its own door — the success line names the
|
|
75
|
+
// right next verb, not `publish` on a workspace that refuses it. Asked before any path
|
|
76
|
+
// that can finish, including the one that collects a pairing started earlier.
|
|
77
|
+
let draftsHere = false;
|
|
78
|
+
try { const j = await (await fetch(`${ORIGIN}/.well-known/augur.json`, { headers: { Accept: "application/json" } })).json(); draftsHere = !!(j && j.drafts && j.drafts.enabled); } catch (e) { draftsHere = false; }
|
|
79
|
+
const TOKENS_FILE = path.join(os.homedir(), ".config", "augur", "tokens.json");
|
|
80
|
+
function savedToken() {
|
|
81
|
+
try { const t = JSON.parse(readFileSync(TOKENS_FILE, "utf8"))[host]; return t && t.token ? t : null; } catch (e) { return null; }
|
|
82
|
+
}
|
|
83
|
+
if (!AGAIN && !process.env.AUGUR_TOKEN) {
|
|
84
|
+
const saved = savedToken();
|
|
85
|
+
if (saved && !(saved.expiresAt && Date.parse(saved.expiresAt) <= Date.now())) {
|
|
86
|
+
let live = false;
|
|
87
|
+
try {
|
|
88
|
+
const r = await fetch(`${ORIGIN}/__unit/drafts`, { headers: { Authorization: `Bearer ${saved.token}`, Accept: "application/json" } });
|
|
89
|
+
live = r.status === 200;
|
|
90
|
+
} catch (e) { live = false; }
|
|
91
|
+
if (live) {
|
|
92
|
+
log(`this machine is already connected to ${C.bold}${host}${C.off}${saved.at ? ` (since ${saved.at.slice(0, 16).replace("T", " ")})` : ""}. Nothing to approve.`);
|
|
93
|
+
console.log(` ${C.dim}\`augur open <opportunity>/<prototype>\` works from here. \`augur connect --again\` pairs afresh.${C.off}`);
|
|
94
|
+
process.exit(0);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
67
99
|
// A pairing this machine already started and nobody has collected: ask once whether it
|
|
68
100
|
// was approved meanwhile, and if not, keep waiting on THAT code rather than minting a
|
|
69
101
|
// second one for the same person to type.
|
|
@@ -152,7 +184,17 @@ all[new URL(ORIGIN).host] = {
|
|
|
152
184
|
writeFileSync(file, JSON.stringify(all, null, 2), { mode: 0o600 });
|
|
153
185
|
|
|
154
186
|
log(`${C.ok}paired — publish access: ${saved.space === "*" ? "all spaces" : saved.space}${C.off}`);
|
|
155
|
-
|
|
187
|
+
// The next verb, spelled so it runs from THIS machine: `augur` after a global install, the
|
|
188
|
+
// package's own `npx` line otherwise — and the verb this workspace takes, which the door
|
|
189
|
+
// knows (drafts: open and land; a whole-tree instance: publish).
|
|
190
|
+
const verb = augurOnPath() ? "augur" : "npx @augurworks/augur";
|
|
191
|
+
if (draftsHere) {
|
|
192
|
+
console.log(`ready — \`${verb} open <opportunity>/<prototype>\` opens one prototype as a draft (live at once at its own address);`);
|
|
193
|
+
console.log(` edit the folder it makes, then \`${verb} land\` in that folder: the real URL moves.`);
|
|
194
|
+
} else {
|
|
195
|
+
console.log(`ready — \`${verb} publish\` will now use this token for ${ORIGIN}`);
|
|
196
|
+
}
|
|
197
|
+
if (verb !== "augur") console.log(`${C.dim}(\`augur\` is not on this machine's PATH; \`npm i -g @augurworks/augur\` puts it there for good.)${C.off}`);
|
|
156
198
|
if (saved.expiresAt) {
|
|
157
199
|
const days = Math.max(0, Math.round((Date.parse(saved.expiresAt) - Date.now()) / 86400000));
|
|
158
200
|
console.log(`${C.dim}It expires in ${days} days (${saved.expiresAt.slice(0, 10)}). Run \`augur connect\` again then.${C.off}`);
|
package/scripts/lib/adapters.mjs
CHANGED
|
@@ -107,7 +107,12 @@ export const hookCommand = (event, { onPath = false } = {}) =>
|
|
|
107
107
|
export function augurOnPath() {
|
|
108
108
|
try {
|
|
109
109
|
const out = execFileSync(process.platform === "win32" ? "where" : "which", ["augur"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
110
|
-
|
|
110
|
+
const found = out.trim().split(/\r?\n/)[0] || "";
|
|
111
|
+
// Inside an `npx` run the package's own bin dir is on PATH and `augur` resolves to a shim
|
|
112
|
+
// in the npx cache — gone the moment npx exits. A hook written as `augur hook pre` from
|
|
113
|
+
// there fails on every later edit (one cold agent's drafts never auto-saved, and its tool
|
|
114
|
+
// showed a "not found" on each write). That resolution counts as NOT on PATH.
|
|
115
|
+
return found.length > 0 && !/[\\/]_npx[\\/]/.test(found);
|
|
111
116
|
} catch (e) { return false; }
|
|
112
117
|
}
|
|
113
118
|
const ours = (h) => !!(h && Array.isArray(h.hooks) && h.hooks.some((x) => x && typeof x.command === "string" && OURS_RE.test(x.command.trim())));
|
package/scripts/open.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import fs from "node:fs";
|
|
|
9
9
|
import path from "node:path";
|
|
10
10
|
import { target, buildStamp } from "./lib/store.mjs";
|
|
11
11
|
import { unitClient, doOpen, unitPathFor } from "./lib/draft.mjs";
|
|
12
|
-
import { installAdapters } from "./lib/adapters.mjs";
|
|
12
|
+
import { installAdapters, augurOnPath } from "./lib/adapters.mjs";
|
|
13
13
|
import { normUnit } from "../src/unit-core.mjs";
|
|
14
14
|
|
|
15
15
|
const log = (m) => console.error(`\x1b[35m[open]\x1b[0m ${m}`);
|
|
@@ -51,7 +51,7 @@ log(r.isNew ? `draft ${r.draftId} on ${unit} — a NEW prototype; ${dir} is empt
|
|
|
51
51
|
// here (idempotent; `AUGUR_NO_ADAPTERS=1` skips it — the suite and CI set it).
|
|
52
52
|
if (!process.env.AUGUR_NO_ADAPTERS) {
|
|
53
53
|
for (const a of installAdapters()) {
|
|
54
|
-
if (a.result === "installed") log(`${a.name}:
|
|
54
|
+
if (a.result === "installed") log(`${a.name}: a save hook is now in ${a.path} — after each edit inside a draft folder it runs \`augur hook post\`, which saves that draft to ${origin}; before an edit it refuses writes into a shared checkout's prototypes. It does nothing outside draft folders and talks to nothing else; \`augur hook remove\` takes it out.`);
|
|
55
55
|
else if (a.result === "updated") log(`${a.name}: editor hooks updated (${a.path}).`);
|
|
56
56
|
}
|
|
57
57
|
}
|
|
@@ -60,4 +60,10 @@ if (r.others.length) {
|
|
|
60
60
|
for (const o of r.others) log(` ${o.session || "someone"} (${o.active ? "active" : "idle"})`);
|
|
61
61
|
log("nothing here stops you; if you both land, the second one syncs first.");
|
|
62
62
|
}
|
|
63
|
+
// The next two steps, spelled so they run from this machine (`augur` only after a global
|
|
64
|
+
// install; the package's `npx` line otherwise).
|
|
65
|
+
const verb = augurOnPath() ? "augur" : "npx @augurworks/augur";
|
|
66
|
+
log(r.isNew
|
|
67
|
+
? `next: write ${path.join(dir, "index.html")} (self-contained static HTML); it is live at the address below as soon as it saves. When it is ready: \`cd ${path.basename(dir)} && ${verb} land\` — the last line printed is the live URL.`
|
|
68
|
+
: `next: edit the files in ${dir}; every save is live at the address below. When it is ready: \`cd ${path.basename(dir)} && ${verb} land\` — the last line printed is the live URL.`);
|
|
63
69
|
console.log(r.address);
|
package/src/_worker.js
CHANGED
|
@@ -3748,16 +3748,23 @@ async function mintPublishToken(kv, tctx, u, { label = null, env = null } = {})
|
|
|
3748
3748
|
* Self-contained, like the login and 404 pages beside it: this must render for somebody
|
|
3749
3749
|
* whose terminal is already waiting, so it depends on no chrome bundle and no space.
|
|
3750
3750
|
*/
|
|
3751
|
-
function connectPage(tctx, me) {
|
|
3751
|
+
function connectPage(tctx, me, origin) {
|
|
3752
3752
|
const body = roleOf(me) === "viewer"
|
|
3753
3753
|
? `<h1>Connect a terminal</h1>
|
|
3754
3754
|
<p>This account can look around but not publish, so it cannot approve a terminal.</p>
|
|
3755
3755
|
<a class="home" href="/">Back to Augur</a>`
|
|
3756
3756
|
: `<h1>Connect a terminal</h1>
|
|
3757
|
-
<p>Type the code your terminal
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3757
|
+
<p>Type the code that your terminal, or the assistant you are working with right now,
|
|
3758
|
+
is showing you. Approving it lets that terminal publish as
|
|
3759
|
+
<strong>${escapeHtml(me.email)}</strong>: it works as you, and everything it lands
|
|
3760
|
+
carries your name.</p>
|
|
3761
|
+
<p class="warn">Only approve a code from a terminal you started or an assistant you
|
|
3762
|
+
are talking to right now. A code that arrives by mail, from a stranger, or out of
|
|
3763
|
+
the blue is not yours — do not type it.</p>
|
|
3764
|
+
<p>Where the code comes from: the terminal or assistant runs
|
|
3765
|
+
<code>npx @augurworks/augur connect --origin ${escapeHtml(origin || "")}</code>, this
|
|
3766
|
+
workspace's own command-line tool, and it shows the code. An assistant that has not
|
|
3767
|
+
shown you one yet can be told to run that.</p>
|
|
3761
3768
|
<form id="pairf">
|
|
3762
3769
|
<input id="pairc" autocomplete="off" autocapitalize="characters" spellcheck="false"
|
|
3763
3770
|
placeholder="ABCD-EFGH" aria-label="Pairing code" />
|
|
@@ -7384,12 +7391,20 @@ function browserFetch(request) {
|
|
|
7384
7391
|
return /^Mozilla\//.test(request.headers.get("User-Agent") || "");
|
|
7385
7392
|
}
|
|
7386
7393
|
|
|
7387
|
-
/**
|
|
7394
|
+
/**
|
|
7395
|
+
* The one paragraph a non-browser fetch of a prototype is given, first in the body.
|
|
7396
|
+
*
|
|
7397
|
+
* DESCRIPTIVE, THIRD PERSON, NO ADDRESS AND NO REASSURANCE. A first draft said "Assistants
|
|
7398
|
+
* and scripts: … a terminal is paired with the person's approval, and nobody is ever asked
|
|
7399
|
+
* for a password", and the third cold agent read exactly that sentence as a prompt
|
|
7400
|
+
* injection — text on a fetched page that speaks to the agent and reassures it is the
|
|
7401
|
+
* shape of an attack, whatever it says. A statement of fact and a link is what a page may
|
|
7402
|
+
* carry: what this is, and where the workspace documents how it is edited.
|
|
7403
|
+
*/
|
|
7388
7404
|
function agentPreface(url) {
|
|
7389
7405
|
const origin = escapeHtml(url.origin);
|
|
7390
|
-
return `<p data-augur-door>This prototype is served by an Augur workspace
|
|
7391
|
-
+ `
|
|
7392
|
-
+ `a terminal is paired with the person's approval, and nobody is ever asked for a password.</p>`;
|
|
7406
|
+
return `<p data-augur-door>This prototype is served by an Augur workspace (${origin}). `
|
|
7407
|
+
+ `The workspace documents how its prototypes are edited at <a href="${DOOR_DOCS}">${origin}${DOOR_DOCS}</a>.</p>`;
|
|
7393
7408
|
}
|
|
7394
7409
|
|
|
7395
7410
|
/**
|
|
@@ -9227,6 +9242,8 @@ function wantsJson(request, url) {
|
|
|
9227
9242
|
// probe and the "an unknown path and the root are the same page" contract depend on it.
|
|
9228
9243
|
const DOOR_DOCS = "/llms.txt";
|
|
9229
9244
|
const DOOR_WELL_KNOWN = "/.well-known/augur.json";
|
|
9245
|
+
/** The engine's public source — what the CLI on npm is built from, named so an agent can check. */
|
|
9246
|
+
const ENGINE_SOURCE = "https://github.com/andratwiro/augur";
|
|
9230
9247
|
|
|
9231
9248
|
/** Does this deployment serve drafts — a unit store beside a bundle store. The same two checks `unitApi` makes. */
|
|
9232
9249
|
const draftsServedHere = (env) => !!(env && env.BUNDLES && unitNamespace(env));
|
|
@@ -9241,6 +9258,10 @@ function doorFacts(tctx, url, env) {
|
|
|
9241
9258
|
engine: tctx.INSTANCE_ENGINE_VERSION ? { version: tctx.INSTANCE_ENGINE_VERSION } : {},
|
|
9242
9259
|
pairing: { enabled: pairing, start: "/__publish/_pair/start", approve: "/__connect" },
|
|
9243
9260
|
connect: pairing ? `npx @augurworks/augur connect --origin ${url.origin}` : null,
|
|
9261
|
+
// Where the CLI comes from, for an agent that checks before it runs anything: the
|
|
9262
|
+
// package is the same open-source engine that serves this page, and `npm view
|
|
9263
|
+
// @augurworks/augur repository` names the same repository.
|
|
9264
|
+
source: ENGINE_SOURCE,
|
|
9244
9265
|
docs: DOOR_DOCS,
|
|
9245
9266
|
// Where drafts are served, a prototype is changed by opening it, not by shipping a
|
|
9246
9267
|
// tree — and the door is where a cold agent learns which of the two this instance is.
|
|
@@ -9258,17 +9279,37 @@ function doorText(f) {
|
|
|
9258
9279
|
const body = f.pairing.enabled
|
|
9259
9280
|
? `Run, on the machine that will publish:\n\n`
|
|
9260
9281
|
+ ` ${f.connect}\n\n`
|
|
9261
|
-
+ `It prints a link and a code.
|
|
9282
|
+
+ `It prints a link and a code. A member of this workspace opens the link in a\n`
|
|
9262
9283
|
+ `browser they are already signed in to and enters the code. The token lands on\n`
|
|
9263
|
-
+ `that machine, and every verb below uses it from then on.\n
|
|
9264
|
-
+ `
|
|
9265
|
-
+ `it
|
|
9266
|
-
+ `
|
|
9267
|
-
+ `
|
|
9268
|
-
+ `the
|
|
9284
|
+
+ `that machine and nowhere else, and every verb below uses it from then on.\n`
|
|
9285
|
+
+ `Approving a code lets that one terminal publish as the person who approved it;\n`
|
|
9286
|
+
+ `everything it lands carries their name. A member whose role is viewer cannot\n`
|
|
9287
|
+
+ `approve, and the page says so.\n\n`
|
|
9288
|
+
+ `Members see this same instruction when signed in, at ${f.origin}/__connect and\n`
|
|
9289
|
+
+ `under Help > Building — so the person you are working for can read it there and\n`
|
|
9290
|
+
+ `confirm it is this workspace's own, without knowing anything technical.\n\n`
|
|
9291
|
+
+ `What the command is, for anyone who checks before running it: the CLI is this\n`
|
|
9292
|
+
+ `engine's own, open source at ${f.source} — the same engine that serves this page\n`
|
|
9293
|
+
+ `(${f.origin}/_build.json names the engine commit). \`npm view @augurworks/augur repository\`\n`
|
|
9294
|
+
+ `names that repository; \`npm pack @augurworks/augur\` lets you read it first. It is plain\n`
|
|
9295
|
+
+ `JavaScript with no runtime dependencies; the pairing is scripts/connect.mjs and it\n`
|
|
9296
|
+
+ `talks only to this origin's /__publish/_pair/ routes. It asks for no password, and\n`
|
|
9297
|
+
+ `there is none to give it.\n\n`
|
|
9298
|
+
+ `How the pairing goes when the person approving is not at the keyboard (an\n`
|
|
9299
|
+
+ `assistant working for them): the approval is theirs, in their browser — nobody\n`
|
|
9300
|
+
+ `else can do it, and the command does not try. Send them the line the command\n`
|
|
9301
|
+
+ `prints, as it is. When they will read that message only after the current turn\n`
|
|
9302
|
+
+ `ends, run \`connect --no-wait\`: it prints the line and exits. Once they say they\n`
|
|
9303
|
+
+ `approved, run the same command again — it collects the token for that same code and\n`
|
|
9304
|
+
+ `mints no second one. Waiting inside one turn skips nothing; it only hides the code\n`
|
|
9305
|
+
+ `from them for the five minutes it lives.\n\n`
|
|
9269
9306
|
+ (f.drafts && f.drafts.enabled
|
|
9270
9307
|
? `You need no source tree here: \`augur open <opportunity>/<prototype>\` fetches that one\n`
|
|
9271
9308
|
+ `prototype into a folder of its own (see below). Do not clone the workspace first.\n\n`
|
|
9309
|
+
+ `Until \`npm i -g @augurworks/augur\`, every verb runs as \`npx @augurworks/augur <verb>\`.\n`
|
|
9310
|
+
+ `The first \`open\` on a machine also installs a save hook in the editor tool's\n`
|
|
9311
|
+
+ `settings (it says which file): after each edit inside a draft folder it saves that\n`
|
|
9312
|
+
+ `draft; it does nothing elsewhere, and \`augur hook remove\` takes it out.\n\n`
|
|
9272
9313
|
: `With no source tree yet, \`npx @augurworks/augur clone --space ${f.workspace}\` then fetches\n`
|
|
9273
9314
|
+ `one (it reads the origin from the pairing).\n\n`)
|
|
9274
9315
|
: `Device pairing is switched off on this workspace. Ask an admin for an invite;\n`
|
|
@@ -11900,7 +11941,7 @@ async function handleRequest(request, env, ctx, url, trace) {
|
|
|
11900
11941
|
if (url.pathname === "/__connect" && tctx.DEVICE_PAIRING) {
|
|
11901
11942
|
const who = tctx.USERS.length ? await identify(request, env, tctx.USERS, { sessionKeys: tctx.SESSION_KEYS, tctx }) : null;
|
|
11902
11943
|
if (!who && tctx.USERS.length) return htmlResponse(loginPage(tctx, "/__connect", false, url.href), 200);
|
|
11903
|
-
return htmlResponse(connectPage(tctx, who), 200);
|
|
11944
|
+
return htmlResponse(connectPage(tctx, who, url.origin), 200);
|
|
11904
11945
|
}
|
|
11905
11946
|
if (url.pathname.startsWith("/__publish/_pair/")) {
|
|
11906
11947
|
// Identity is resolved here rather than reusing the gate's `me` below, because this
|
package/src/chrome/appchrome.mjs
CHANGED
|
@@ -386,10 +386,16 @@ function helpDrawer(state) {
|
|
|
386
386
|
<li>The design system is read-only from a prototype. Edit the skill's own source rather than copying its classes out.</li>
|
|
387
387
|
</ul>
|
|
388
388
|
|
|
389
|
+
<h4>Working with an assistant</h4>
|
|
390
|
+
<ul>
|
|
391
|
+
<li>Give it this workspace's address and say what you want changed or made. It reads how to get in from this site and hands you a link and a short code.</li>
|
|
392
|
+
<li>Open the link here (<code>/__connect</code>) and type the code. That pairs its terminal to publish as you; everything it lands carries your name.</li>
|
|
393
|
+
<li>From then on it opens a prototype as a draft, edits it live at the draft's own address, and lands it when you say so. Viewers cannot approve a code.</li>
|
|
394
|
+
</ul>
|
|
395
|
+
|
|
389
396
|
<h4>Comment loop <span class="gvhelp__tag">maintainer</span></h4>
|
|
390
397
|
<ul>
|
|
391
|
-
<li
|
|
392
|
-
<li>The agent fixes, replies, resolves in-thread. Put it on <code>/loop</code> to keep watching.</li>
|
|
398
|
+
<li>In a checkout, <code>npm run review --open</code> lists open threads; an agent fixes, replies and resolves in-thread.</li>
|
|
393
399
|
<li>Not automated. You steer it.</li>
|
|
394
400
|
</ul>${spaceHelpSections(state)}
|
|
395
401
|
</section>
|