cherry-works 0.1.0
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 +253 -0
- package/dist/cw.js +116 -0
- package/dist/main.js +4086 -0
- package/dist/portal/index.html +11 -0
- package/dist/portal/logo.png +0 -0
- package/dist/portal/main.js +227 -0
- package/dist/portal/styles.css +2 -0
- package/package.json +97 -0
package/dist/main.js
ADDED
|
@@ -0,0 +1,4086 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// main.ts
|
|
4
|
+
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
5
|
+
|
|
6
|
+
// package.json
|
|
7
|
+
var package_default = {
|
|
8
|
+
name: "cherry-works",
|
|
9
|
+
version: "0.1.0",
|
|
10
|
+
description: "The charter engine: resolves a charter and compiles it into what a coding agent reads.",
|
|
11
|
+
keywords: [
|
|
12
|
+
"claude-code",
|
|
13
|
+
"coding-agent",
|
|
14
|
+
"charter",
|
|
15
|
+
"ai-agents",
|
|
16
|
+
"cli",
|
|
17
|
+
"guardrails"
|
|
18
|
+
],
|
|
19
|
+
homepage: "https://github.com/tamnguyenvt/cherry-works#readme",
|
|
20
|
+
bugs: {
|
|
21
|
+
url: "https://github.com/tamnguyenvt/cherry-works/issues"
|
|
22
|
+
},
|
|
23
|
+
repository: {
|
|
24
|
+
type: "git",
|
|
25
|
+
url: "git+https://github.com/tamnguyenvt/cherry-works.git"
|
|
26
|
+
},
|
|
27
|
+
license: "MIT",
|
|
28
|
+
author: "Tam Nguyen",
|
|
29
|
+
type: "module",
|
|
30
|
+
engines: {
|
|
31
|
+
node: ">=22",
|
|
32
|
+
pnpm: ">=10"
|
|
33
|
+
},
|
|
34
|
+
packageManager: "pnpm@10.2.1",
|
|
35
|
+
bin: {
|
|
36
|
+
cw: "./dist/cw.js"
|
|
37
|
+
},
|
|
38
|
+
files: [
|
|
39
|
+
"dist"
|
|
40
|
+
],
|
|
41
|
+
imports: {
|
|
42
|
+
"#hexagon/*": "./src/hexagon/*",
|
|
43
|
+
"#portal/*": "./dist/portal/*"
|
|
44
|
+
},
|
|
45
|
+
scripts: {
|
|
46
|
+
build: "tsup && tailwindcss -i src/driver/portal/page/styles.css -o dist/portal/styles.css --minify",
|
|
47
|
+
typecheck: "tsc --noEmit && tsc --noEmit -p src/driver/portal/page",
|
|
48
|
+
test: 'pnpm run lint:deps && pnpm run build && node --import tsx --test "test/**/*.test.ts"',
|
|
49
|
+
cw: "node --import tsx main.ts",
|
|
50
|
+
"lint:deps": "depcruise src main.ts",
|
|
51
|
+
"specs:link": "node --import tsx scripts/link-specs.ts",
|
|
52
|
+
"specs:check": "node --import tsx scripts/link-specs.ts --check",
|
|
53
|
+
release: "node --import tsx scripts/release.ts",
|
|
54
|
+
prepublishOnly: `node -e "console.error('Publish with \\"pnpm release <version>\\", which checks the package first.'); process.exit(1)"`
|
|
55
|
+
},
|
|
56
|
+
devDependencies: {
|
|
57
|
+
"@codemirror/commands": "^6.11.1",
|
|
58
|
+
"@codemirror/lang-markdown": "^6.5.2",
|
|
59
|
+
"@codemirror/language": "^6.12.4",
|
|
60
|
+
"@codemirror/state": "^6.7.4",
|
|
61
|
+
"@codemirror/view": "^6.43.11",
|
|
62
|
+
"@tailwindcss/cli": "^4.3.3",
|
|
63
|
+
"@tanstack/react-query": "^5.103.2",
|
|
64
|
+
"@types/node": "^22.10.2",
|
|
65
|
+
"@types/picomatch": "^4.0.3",
|
|
66
|
+
"@types/prompts": "^2.4.9",
|
|
67
|
+
"@types/react": "^19.3.0",
|
|
68
|
+
"@types/react-dom": "^19.3.0",
|
|
69
|
+
"@types/yargs": "^17.0.35",
|
|
70
|
+
"class-variance-authority": "^0.7.1",
|
|
71
|
+
clsx: "^2.1.1",
|
|
72
|
+
cmdk: "^1.1.1",
|
|
73
|
+
"dependency-cruiser": "^18.2.0",
|
|
74
|
+
"lucide-react": "^1.47.0",
|
|
75
|
+
marked: "^18.0.13",
|
|
76
|
+
playwright: "^1.63.0",
|
|
77
|
+
"radix-ui": "^1.6.7",
|
|
78
|
+
react: "^19.3.0",
|
|
79
|
+
"react-dom": "^19.3.0",
|
|
80
|
+
sonner: "^2.0.8",
|
|
81
|
+
"tailwind-merge": "^3.7.0",
|
|
82
|
+
tailwindcss: "^4.3.3",
|
|
83
|
+
tsup: "^8.3.5",
|
|
84
|
+
tsx: "^4.19.2",
|
|
85
|
+
"tw-animate-css": "^1.4.0",
|
|
86
|
+
typescript: "^5.7.2"
|
|
87
|
+
},
|
|
88
|
+
pnpm: {
|
|
89
|
+
onlyBuiltDependencies: [
|
|
90
|
+
"esbuild"
|
|
91
|
+
]
|
|
92
|
+
},
|
|
93
|
+
dependencies: {
|
|
94
|
+
"@hono/node-server": "^2.1.1",
|
|
95
|
+
"@hono/zod-openapi": "^1.6.3",
|
|
96
|
+
hono: "^4.13.7",
|
|
97
|
+
picomatch: "^4.0.7",
|
|
98
|
+
prompts: "^2.4.2",
|
|
99
|
+
yaml: "^2.9.0",
|
|
100
|
+
yargs: "^18.1.0",
|
|
101
|
+
zod: "^4.6.1"
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// src/driver/cli/Commander.ts
|
|
106
|
+
import yargs from "yargs";
|
|
107
|
+
|
|
108
|
+
// src/hexagon/domain/models/Fault.ts
|
|
109
|
+
var Fault = class extends Error {
|
|
110
|
+
constructor(message, fix, severity = "error") {
|
|
111
|
+
super(message);
|
|
112
|
+
this.fix = fix;
|
|
113
|
+
this.severity = severity;
|
|
114
|
+
this.name = new.target.name;
|
|
115
|
+
}
|
|
116
|
+
fix;
|
|
117
|
+
severity;
|
|
118
|
+
};
|
|
119
|
+
var CharterRootFault = class extends Fault {
|
|
120
|
+
};
|
|
121
|
+
var SettingsFault = class extends Fault {
|
|
122
|
+
};
|
|
123
|
+
var CharterPrimitiveFault = class extends Fault {
|
|
124
|
+
};
|
|
125
|
+
var TestSuiteFault = class extends Fault {
|
|
126
|
+
};
|
|
127
|
+
var TestCaseFault = class extends Fault {
|
|
128
|
+
};
|
|
129
|
+
var VendorFault = class extends Fault {
|
|
130
|
+
};
|
|
131
|
+
function throwAggregateError(faults2) {
|
|
132
|
+
throw new AggregateError(faults2, faults2.map((one) => one.message).join(" "));
|
|
133
|
+
}
|
|
134
|
+
var FaultsByFile = class _FaultsByFile {
|
|
135
|
+
/** Nothing wrong with any file. */
|
|
136
|
+
static none = new _FaultsByFile({});
|
|
137
|
+
#files;
|
|
138
|
+
constructor(files) {
|
|
139
|
+
this.#files = files;
|
|
140
|
+
}
|
|
141
|
+
/** Every fault, under the file it is wrong with. */
|
|
142
|
+
get files() {
|
|
143
|
+
return this.#files;
|
|
144
|
+
}
|
|
145
|
+
/** Is no file wrong at all? */
|
|
146
|
+
get isEmpty() {
|
|
147
|
+
return Object.keys(this.#files).length === 0;
|
|
148
|
+
}
|
|
149
|
+
/** These and the other's together, a file both name holding the faults of
|
|
150
|
+
* both. */
|
|
151
|
+
with(other) {
|
|
152
|
+
const files = { ...this.#files };
|
|
153
|
+
for (const [file, faults2] of Object.entries(other.#files)) files[file] = [...files[file] ?? [], ...faults2];
|
|
154
|
+
return new _FaultsByFile(files);
|
|
155
|
+
}
|
|
156
|
+
/** What is wrong badly enough to stop a build, under the file that has to
|
|
157
|
+
* change. A file whose every fault is a warning is not in here at all
|
|
158
|
+
* (FR-005). */
|
|
159
|
+
errors() {
|
|
160
|
+
return new _FaultsByFile(
|
|
161
|
+
Object.fromEntries(
|
|
162
|
+
Object.entries(this.#files).map(([file, faults2]) => [file, faults2.filter((one) => one.severity === "error")]).filter(([, faults2]) => faults2.length > 0)
|
|
163
|
+
)
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
/** The same faults, each file named the way an author would type it: where
|
|
167
|
+
* it sits under the repository they ran in, where the file is the `href` it
|
|
168
|
+
* was read from. */
|
|
169
|
+
namedFrom(repo) {
|
|
170
|
+
return new _FaultsByFile(Object.fromEntries(Object.entries(this.#files).map(([file, faults2]) => [nameOf(file, repo), faults2])));
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
var Faults = class {
|
|
174
|
+
constructor(faults2) {
|
|
175
|
+
this.faults = faults2;
|
|
176
|
+
}
|
|
177
|
+
faults;
|
|
178
|
+
};
|
|
179
|
+
function nameOf(file, repo) {
|
|
180
|
+
const at = `${repo.href.replace(/\/$/, "")}/`;
|
|
181
|
+
return decodeURIComponent(file.startsWith(at) ? file.slice(at.length) : file);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// src/hexagon/domain/models/AgentProvider.ts
|
|
185
|
+
var AGENT_PROVIDERS = ["claude"];
|
|
186
|
+
function isAgentProvider(name) {
|
|
187
|
+
return typeof name === "string" && AGENT_PROVIDERS.includes(name);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// src/hexagon/utils/globs.ts
|
|
191
|
+
import picomatch from "picomatch";
|
|
192
|
+
function covers(outer, inner) {
|
|
193
|
+
return outer === inner || matches(outer, inner);
|
|
194
|
+
}
|
|
195
|
+
function matches(glob, path) {
|
|
196
|
+
return picomatch(glob, { dot: true })(path);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// src/hexagon/domain/models/charter/primitive/Primitive.ts
|
|
200
|
+
import { z as z4 } from "zod";
|
|
201
|
+
|
|
202
|
+
// src/hexagon/domain/models/charter/primitive/BasePrimitive.ts
|
|
203
|
+
import { z } from "zod";
|
|
204
|
+
|
|
205
|
+
// src/hexagon/domain/models/helper.ts
|
|
206
|
+
function formatFrontmatterValue(value) {
|
|
207
|
+
return Array.isArray(value) ? `[${value.map((one) => JSON.stringify(one)).join(", ")}]` : String(value);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// src/hexagon/domain/models/charter/primitive/BasePrimitive.ts
|
|
211
|
+
var DELIMITER = "---";
|
|
212
|
+
var goodLine = z.string().regex(/\S/);
|
|
213
|
+
var goodArray = z.array(goodLine).min(1).readonly();
|
|
214
|
+
var CommonHeaders = z.object({
|
|
215
|
+
id: z.string().regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/),
|
|
216
|
+
/** All an agent reads of a primitive before opening its body. */
|
|
217
|
+
description: goodLine,
|
|
218
|
+
tags: z.array(goodLine).readonly().optional(),
|
|
219
|
+
/** The files this primitive speaks about, where it speaks about any. A guide's
|
|
220
|
+
* are what decide when its body is loaded; a mixin may name them to say how
|
|
221
|
+
* far what it lends reaches (FR-006). */
|
|
222
|
+
globs: z.array(goodLine).readonly().optional(),
|
|
223
|
+
/** `corpus:<id>`, the reasoning this primitive cites (FR-005). */
|
|
224
|
+
rationale: goodLine.optional(),
|
|
225
|
+
mixins: z.array(goodLine).readonly().optional()
|
|
226
|
+
});
|
|
227
|
+
var BasePrimitive = class {
|
|
228
|
+
constructor(headers, body) {
|
|
229
|
+
this.headers = headers;
|
|
230
|
+
this.body = body;
|
|
231
|
+
}
|
|
232
|
+
headers;
|
|
233
|
+
body;
|
|
234
|
+
/** What every kind requires of its author, whatever else its own contract
|
|
235
|
+
* asks for. A kind's own `requires` stands beside this rather than adding to
|
|
236
|
+
* it, so whoever asks for a kind's headers asks for both (FR-039). `id` is
|
|
237
|
+
* not among them: a primitive is named where it is asked for rather than
|
|
238
|
+
* answered for, so what is left to ask about is what it is for. */
|
|
239
|
+
static requires = { description: "line" };
|
|
240
|
+
/** The schema every kind's headers are read by, its own standing in place of
|
|
241
|
+
* this. What a file is refused against, and the one place saying what shape
|
|
242
|
+
* each header holds — including the ones a kind takes without requiring, so
|
|
243
|
+
* whoever is writing a header reads its shape off the same contract a file
|
|
244
|
+
* is refused against (FR-004). */
|
|
245
|
+
static schema = CommonHeaders;
|
|
246
|
+
/**
|
|
247
|
+
* This primitive as the file it is authored in: its headers between the
|
|
248
|
+
* delimiters, and its body under them.
|
|
249
|
+
*
|
|
250
|
+
* The other direction of the reading `primitiveOf` does, and the same
|
|
251
|
+
* frontmatter, so a file written here is a file that reads back as this
|
|
252
|
+
* primitive. Said here rather than by whoever writes, for the reason
|
|
253
|
+
* `description` is: what a primitive holds is the primitive's own, and a
|
|
254
|
+
* caller holding one already has everything the file says.
|
|
255
|
+
*
|
|
256
|
+
* The kind leads, since it is what decides how the rest is read (FR-003). A
|
|
257
|
+
* list is written the way the parser gives it back, so an author opening the
|
|
258
|
+
* file finds what they declared rather than a second spelling of it.
|
|
259
|
+
*/
|
|
260
|
+
toMarkdown() {
|
|
261
|
+
return [
|
|
262
|
+
DELIMITER,
|
|
263
|
+
...Object.entries({ kind: this.kind, ...this.headers }).map(([field, value]) => `${field}: ${formatFrontmatterValue(value)}`),
|
|
264
|
+
DELIMITER,
|
|
265
|
+
"",
|
|
266
|
+
this.body
|
|
267
|
+
].join("\n").replace(/\n*$/, "\n");
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* The one line this primitive is found by, composed: what its author wrote it
|
|
271
|
+
* is for, and — where its kind decides when it applies — what it applies to.
|
|
272
|
+
*
|
|
273
|
+
* Said here rather than by whoever compiles, because what makes a kind
|
|
274
|
+
* activate is the kind's own contract (FR-001, FR-004): a skill's triggers are
|
|
275
|
+
* the author's words, which only a skill knows how to say in a line. A kind
|
|
276
|
+
* that nothing activates on its description is the description it was
|
|
277
|
+
* written with.
|
|
278
|
+
*/
|
|
279
|
+
description() {
|
|
280
|
+
return this.headers.description;
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
function headersOf(sampledKind, headerSchema, record) {
|
|
284
|
+
const parsed = headerSchema.safeParse(record);
|
|
285
|
+
if (parsed.success) return parsed.data;
|
|
286
|
+
const { kind, sample } = sampledKind;
|
|
287
|
+
const sampleLines = Object.entries({ kind, ...sample }).map(([field, value]) => `${field}: ${formatFrontmatterValue(value)}`);
|
|
288
|
+
return throwAggregateError([
|
|
289
|
+
new CharterPrimitiveFault(
|
|
290
|
+
`These headers are not what a ${kind} holds.`,
|
|
291
|
+
`Write them the way this ${kind} does:
|
|
292
|
+
${sampleLines.join("\n")}`
|
|
293
|
+
)
|
|
294
|
+
]);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// src/hexagon/domain/models/charter/primitive/AgentPrimitive.ts
|
|
298
|
+
var AgentHeaders = CommonHeaders.extend({
|
|
299
|
+
tools: goodArray
|
|
300
|
+
});
|
|
301
|
+
var AgentPrimitive = class _AgentPrimitive extends BasePrimitive {
|
|
302
|
+
static kind = "agent";
|
|
303
|
+
kind = _AgentPrimitive.kind;
|
|
304
|
+
/** What this kind requires beyond the common headers, declared where its
|
|
305
|
+
* contract is: it is read both where a file is checked and where one is
|
|
306
|
+
* scaffolded (FR-004, FR-039). */
|
|
307
|
+
static requires = { tools: "list" };
|
|
308
|
+
/** The schema its headers are read by: what `headersOf` refuses a file
|
|
309
|
+
* against, and what says the shape of every header this kind takes, required
|
|
310
|
+
* or not (FR-004). */
|
|
311
|
+
static schema = AgentHeaders;
|
|
312
|
+
/** When a reader of this charter is to open this kind at all, said
|
|
313
|
+
* where the kind's contract is: the neutral surface lists one
|
|
314
|
+
* line per kind and none of them is written down twice (FR-002). */
|
|
315
|
+
static activatesWhen = "it is spawned by identity, holding the `tools` it lists and nothing else";
|
|
316
|
+
/** An agent as an author writes one: what a refused header is fixed with. */
|
|
317
|
+
static sample = {
|
|
318
|
+
id: "reviewer",
|
|
319
|
+
description: "Review a diff and name what is wrong with it.",
|
|
320
|
+
tools: ["Read", "Grep"]
|
|
321
|
+
};
|
|
322
|
+
/** One agent, or every fault its headers have (FR-004). */
|
|
323
|
+
static of(record, body) {
|
|
324
|
+
return new _AgentPrimitive(headersOf(_AgentPrimitive, AgentHeaders, record), body);
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
// src/hexagon/domain/models/charter/primitive/CommandPrimitive.ts
|
|
329
|
+
var CommandPrimitive = class _CommandPrimitive extends BasePrimitive {
|
|
330
|
+
static kind = "command";
|
|
331
|
+
kind = _CommandPrimitive.kind;
|
|
332
|
+
/** What this kind requires beyond the common headers: nothing. Declared all
|
|
333
|
+
* the same, since what a file is scaffolded with is read off this (FR-039). */
|
|
334
|
+
static requires = {};
|
|
335
|
+
/** The schema its headers are read by: what `headersOf` refuses a file
|
|
336
|
+
* against, and what says the shape of every header this kind takes, required
|
|
337
|
+
* or not (FR-004). */
|
|
338
|
+
static schema = CommonHeaders;
|
|
339
|
+
/** When a reader of this charter is to open this kind at all, said
|
|
340
|
+
* where the kind's contract is: the neutral surface lists one
|
|
341
|
+
* line per kind and none of them is written down twice (FR-002). */
|
|
342
|
+
static activatesWhen = "it is asked for by name, or a request matches what it describes itself as being for";
|
|
343
|
+
/** A command as an author writes one: what a refused header is fixed with. */
|
|
344
|
+
static sample = {
|
|
345
|
+
id: "release",
|
|
346
|
+
description: "Cut a release from the main branch."
|
|
347
|
+
};
|
|
348
|
+
/** One command, or every fault its headers have. Nothing beyond what every
|
|
349
|
+
* primitive declares: a command is asked for by name, and the one line it
|
|
350
|
+
* describes itself with is what decides that it is the one being asked for. */
|
|
351
|
+
static of(record, body) {
|
|
352
|
+
return new _CommandPrimitive(headersOf(_CommandPrimitive, CommonHeaders, record), body);
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
// src/hexagon/domain/models/charter/primitive/CorpusPrimitive.ts
|
|
357
|
+
var CorpusHeaders = CommonHeaders.extend({});
|
|
358
|
+
var CorpusPrimitive = class _CorpusPrimitive extends BasePrimitive {
|
|
359
|
+
static kind = "corpus";
|
|
360
|
+
kind = _CorpusPrimitive.kind;
|
|
361
|
+
/** What this kind requires beyond the common headers: nothing. Declared all
|
|
362
|
+
* the same, since what a file is scaffolded with is read off this (FR-039). */
|
|
363
|
+
static requires = {};
|
|
364
|
+
/** The schema its headers are read by: what `headersOf` refuses a file
|
|
365
|
+
* against, and what says the shape of every header this kind takes, required
|
|
366
|
+
* or not (FR-004). */
|
|
367
|
+
static schema = CorpusHeaders;
|
|
368
|
+
/** When a reader of this charter is to open this kind at all, said
|
|
369
|
+
* where the kind's contract is: the neutral surface lists one
|
|
370
|
+
* line per kind and none of them is written down twice (FR-002). */
|
|
371
|
+
static activatesWhen = "a primitive's `rationale` cites it \u2014 the reasoning, read when someone asks why";
|
|
372
|
+
/** A corpus as an author writes one: what a refused header is fixed with. */
|
|
373
|
+
static sample = {
|
|
374
|
+
id: "type-safety",
|
|
375
|
+
description: "Why the charter insists on types."
|
|
376
|
+
};
|
|
377
|
+
/** One corpus, or every fault its headers have (FR-004). */
|
|
378
|
+
static of(record, body) {
|
|
379
|
+
return new _CorpusPrimitive(headersOf(_CorpusPrimitive, CorpusHeaders, record), body);
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
// src/hexagon/domain/models/charter/primitive/GuidePrimitive.ts
|
|
384
|
+
var GuideHeaders = CommonHeaders.extend({});
|
|
385
|
+
var GuidePrimitive = class _GuidePrimitive extends BasePrimitive {
|
|
386
|
+
static kind = "guide";
|
|
387
|
+
kind = _GuidePrimitive.kind;
|
|
388
|
+
/** What this kind requires beyond the common headers: nothing. Declared all
|
|
389
|
+
* the same, since what a file is scaffolded with is read off this (FR-039). */
|
|
390
|
+
static requires = {};
|
|
391
|
+
/** The schema its headers are read by: what `headersOf` refuses a file
|
|
392
|
+
* against, and what says the shape of every header this kind takes, required
|
|
393
|
+
* or not (FR-004). */
|
|
394
|
+
static schema = GuideHeaders;
|
|
395
|
+
/** When a reader of this charter is to open this kind at all, said
|
|
396
|
+
* where the kind's contract is: the neutral surface lists one
|
|
397
|
+
* line per kind and none of them is written down twice (FR-002). */
|
|
398
|
+
static activatesWhen = "a touched file matches one of its `globs`, or every turn where globs are not specified";
|
|
399
|
+
/** A guide as an author writes one: what a refused header is fixed with. */
|
|
400
|
+
static sample = {
|
|
401
|
+
id: "no-any",
|
|
402
|
+
description: "Reject the any type in application code.",
|
|
403
|
+
globs: ["src/**/*.ts"],
|
|
404
|
+
tags: ["typescript"],
|
|
405
|
+
rationale: "corpus:type-safety"
|
|
406
|
+
};
|
|
407
|
+
/** One guide, or every fault its headers have (FR-004). */
|
|
408
|
+
static of(record, body) {
|
|
409
|
+
return new _GuidePrimitive(headersOf(_GuidePrimitive, GuideHeaders, record), body);
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
// src/hexagon/domain/models/charter/primitive/MixinPrimitive.ts
|
|
414
|
+
import { z as z2 } from "zod";
|
|
415
|
+
var MixinHeaders = CommonHeaders.extend({
|
|
416
|
+
/** Never: reading refuses a mixin that declares one. */
|
|
417
|
+
mixins: z2.undefined().optional()
|
|
418
|
+
});
|
|
419
|
+
var MixinPrimitive = class _MixinPrimitive extends BasePrimitive {
|
|
420
|
+
static kind = "mixin";
|
|
421
|
+
kind = _MixinPrimitive.kind;
|
|
422
|
+
/** What this kind requires beyond the common headers: nothing. Declared all
|
|
423
|
+
* the same, since what a file is scaffolded with is read off this (FR-039). */
|
|
424
|
+
static requires = {};
|
|
425
|
+
/** The schema its headers are read by: what `headersOf` refuses a file
|
|
426
|
+
* against, and what says the shape of every header this kind takes, required
|
|
427
|
+
* or not (FR-004). */
|
|
428
|
+
static schema = MixinHeaders;
|
|
429
|
+
/** When a reader of this charter is to open this kind at all, said
|
|
430
|
+
* where the kind's contract is: the neutral surface lists one
|
|
431
|
+
* line per kind and none of them is written down twice (FR-002). */
|
|
432
|
+
static activatesWhen = "never on its own: its body is lent to the primitives that pull it in";
|
|
433
|
+
/** A mixin as an author writes one: what a refused header is fixed with. */
|
|
434
|
+
static sample = {
|
|
435
|
+
id: "typescript-scope",
|
|
436
|
+
description: "What counts as TypeScript application code.",
|
|
437
|
+
globs: ["src/**/*.ts"]
|
|
438
|
+
};
|
|
439
|
+
/** One mixin, or every fault its headers have (FR-004, FR-006). */
|
|
440
|
+
static of(record, body) {
|
|
441
|
+
return new _MixinPrimitive(headersOf(_MixinPrimitive, MixinHeaders, record), body);
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
// src/hexagon/domain/models/charter/primitive/PlaybookPrimitive.ts
|
|
446
|
+
var PlaybookHeaders = CommonHeaders.extend({
|
|
447
|
+
triggers: goodArray
|
|
448
|
+
});
|
|
449
|
+
var PlaybookPrimitive = class _PlaybookPrimitive extends BasePrimitive {
|
|
450
|
+
static kind = "playbook";
|
|
451
|
+
kind = _PlaybookPrimitive.kind;
|
|
452
|
+
/** What this kind requires beyond the common headers, declared where its
|
|
453
|
+
* contract is: it is read both where a file is checked and where one is
|
|
454
|
+
* scaffolded (FR-004, FR-039). */
|
|
455
|
+
static requires = { triggers: "list" };
|
|
456
|
+
/** The schema its headers are read by: what `headersOf` refuses a file
|
|
457
|
+
* against, and what says the shape of every header this kind takes, required
|
|
458
|
+
* or not (FR-004). */
|
|
459
|
+
static schema = PlaybookHeaders;
|
|
460
|
+
/** When a reader of this charter is to open this kind at all, said
|
|
461
|
+
* where the kind's contract is: the neutral surface lists one
|
|
462
|
+
* line per kind and none of them is written down twice (FR-002). */
|
|
463
|
+
static activatesWhen = "one of its `triggers` matches; the body is a sequence of commands";
|
|
464
|
+
/** A playbook as an author writes one: what a refused header is fixed with. */
|
|
465
|
+
static sample = {
|
|
466
|
+
id: "ship-a-feature",
|
|
467
|
+
description: "Take a finished branch from review to release.",
|
|
468
|
+
triggers: ["ship this", "release this feature"]
|
|
469
|
+
};
|
|
470
|
+
/** What this is found by: what it is for, and the requests its author said
|
|
471
|
+
* bring it up. */
|
|
472
|
+
description() {
|
|
473
|
+
return `${this.headers.description} Use when: ${this.headers.triggers.join("; ")}.`;
|
|
474
|
+
}
|
|
475
|
+
/** One playbook, or every fault its headers have (FR-004). */
|
|
476
|
+
static of(record, body) {
|
|
477
|
+
return new _PlaybookPrimitive(headersOf(_PlaybookPrimitive, PlaybookHeaders, record), body);
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
// src/hexagon/domain/models/charter/primitive/PosturePrimitive.ts
|
|
482
|
+
var PostureHeaders = CommonHeaders.extend({
|
|
483
|
+
allow: goodArray,
|
|
484
|
+
deny: goodArray
|
|
485
|
+
});
|
|
486
|
+
var PosturePrimitive = class _PosturePrimitive extends BasePrimitive {
|
|
487
|
+
static kind = "posture";
|
|
488
|
+
kind = _PosturePrimitive.kind;
|
|
489
|
+
/** What this kind requires beyond the common headers, declared where its
|
|
490
|
+
* contract is: it is read both where a file is checked and where one is
|
|
491
|
+
* scaffolded (FR-004, FR-039). */
|
|
492
|
+
static requires = { allow: "list", deny: "list" };
|
|
493
|
+
/** The schema its headers are read by: what `headersOf` refuses a file
|
|
494
|
+
* against, and what says the shape of every header this kind takes, required
|
|
495
|
+
* or not (FR-004). */
|
|
496
|
+
static schema = PostureHeaders;
|
|
497
|
+
/** When a reader of this charter is to open this kind at all, said
|
|
498
|
+
* where the kind's contract is: the neutral surface lists one
|
|
499
|
+
* line per kind and none of them is written down twice (FR-002). */
|
|
500
|
+
static activatesWhen = "always, wherever the host can be told what to `allow` and what to `deny`";
|
|
501
|
+
/** A posture as an author writes one: what a refused header is fixed with. */
|
|
502
|
+
static sample = {
|
|
503
|
+
id: "no-force-push",
|
|
504
|
+
description: "Never rewrite history someone else may have pulled.",
|
|
505
|
+
allow: ["Bash(git push:*)"],
|
|
506
|
+
deny: ["Bash(git push --force:*)"]
|
|
507
|
+
};
|
|
508
|
+
/** One posture, or every fault its headers have (FR-004). */
|
|
509
|
+
static of(record, body) {
|
|
510
|
+
return new _PosturePrimitive(headersOf(_PosturePrimitive, PostureHeaders, record), body);
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
// src/hexagon/domain/models/charter/primitive/SensorPrimitive.ts
|
|
515
|
+
import { z as z3 } from "zod";
|
|
516
|
+
var SIGNALS = [
|
|
517
|
+
"PreToolUse",
|
|
518
|
+
"PostToolUse",
|
|
519
|
+
"UserPromptSubmit",
|
|
520
|
+
"Notification",
|
|
521
|
+
"Stop",
|
|
522
|
+
"SubagentStop",
|
|
523
|
+
"PreCompact",
|
|
524
|
+
"SessionStart",
|
|
525
|
+
"SessionEnd"
|
|
526
|
+
];
|
|
527
|
+
var SensorHeaders = CommonHeaders.extend({
|
|
528
|
+
/** What raises it: one of the events every host of this engine speaks in. */
|
|
529
|
+
signal: z3.enum(SIGNALS),
|
|
530
|
+
/** What is run when it does. A sensor that names no command is a sensor
|
|
531
|
+
* nothing can fire, so this is required. */
|
|
532
|
+
run: goodLine
|
|
533
|
+
});
|
|
534
|
+
var SensorPrimitive = class _SensorPrimitive extends BasePrimitive {
|
|
535
|
+
static kind = "sensor";
|
|
536
|
+
kind = _SensorPrimitive.kind;
|
|
537
|
+
/** What this kind requires beyond the common headers, declared where its
|
|
538
|
+
* contract is: it is read both where a file is checked and where one is
|
|
539
|
+
* scaffolded (FR-004, FR-039). */
|
|
540
|
+
static requires = { signal: "line", run: "line" };
|
|
541
|
+
/** The schema its headers are read by: what `headersOf` refuses a file
|
|
542
|
+
* against, and what says the shape of every header this kind takes, required
|
|
543
|
+
* or not (FR-004). */
|
|
544
|
+
static schema = SensorHeaders;
|
|
545
|
+
/** When a reader of this charter is to open this kind at all, said
|
|
546
|
+
* where the kind's contract is: the neutral surface lists one
|
|
547
|
+
* line per kind and none of them is written down twice (FR-002). */
|
|
548
|
+
static activatesWhen = "the `signal` it names is raised, and the harness runs what it says to `run`";
|
|
549
|
+
/** A sensor as an author writes one: what a refused header is fixed with. */
|
|
550
|
+
static sample = {
|
|
551
|
+
id: "test-on-stop",
|
|
552
|
+
description: "Run the tests when the agent stops.",
|
|
553
|
+
signal: "Stop",
|
|
554
|
+
run: "pnpm test"
|
|
555
|
+
};
|
|
556
|
+
/** One sensor, or every fault its headers have (FR-004). */
|
|
557
|
+
static of(record, body) {
|
|
558
|
+
return new _SensorPrimitive(headersOf(_SensorPrimitive, SensorHeaders, record), body);
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
// src/hexagon/domain/models/charter/primitive/SkillPrimitive.ts
|
|
563
|
+
var SkillHeaders = CommonHeaders.extend({
|
|
564
|
+
triggers: goodArray
|
|
565
|
+
});
|
|
566
|
+
var SkillPrimitive = class _SkillPrimitive extends BasePrimitive {
|
|
567
|
+
static kind = "skill";
|
|
568
|
+
kind = _SkillPrimitive.kind;
|
|
569
|
+
/** What this kind requires beyond the common headers, declared where its
|
|
570
|
+
* contract is: it is read both where a file is checked and where one is
|
|
571
|
+
* scaffolded (FR-004, FR-039). */
|
|
572
|
+
static requires = { triggers: "list" };
|
|
573
|
+
/** The schema its headers are read by: what `headersOf` refuses a file
|
|
574
|
+
* against, and what says the shape of every header this kind takes, required
|
|
575
|
+
* or not (FR-004). */
|
|
576
|
+
static schema = SkillHeaders;
|
|
577
|
+
/** When a reader of this charter is to open this kind at all, said
|
|
578
|
+
* where the kind's contract is: the neutral surface lists one
|
|
579
|
+
* line per kind and none of them is written down twice (FR-002). */
|
|
580
|
+
static activatesWhen = "one of its `triggers` matches what is being asked";
|
|
581
|
+
/** A skill as an author writes one: what a refused header is fixed with. */
|
|
582
|
+
static sample = {
|
|
583
|
+
id: "refactoring",
|
|
584
|
+
description: "Change how code is structured without changing what it does.",
|
|
585
|
+
triggers: ["refactor this", "clean this up"]
|
|
586
|
+
};
|
|
587
|
+
/** What this is found by: what it is for, and the requests its author said
|
|
588
|
+
* bring it up. */
|
|
589
|
+
description() {
|
|
590
|
+
return `${this.headers.description} Use when: ${this.headers.triggers.join("; ")}.`;
|
|
591
|
+
}
|
|
592
|
+
/** One skill, or every fault its headers have (FR-004). */
|
|
593
|
+
static of(record, body) {
|
|
594
|
+
return new _SkillPrimitive(headersOf(_SkillPrimitive, SkillHeaders, record), body);
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
// src/hexagon/domain/models/charter/primitive/Primitive.ts
|
|
599
|
+
var PRIMITIVE_CLASSES = [
|
|
600
|
+
GuidePrimitive,
|
|
601
|
+
SensorPrimitive,
|
|
602
|
+
CommandPrimitive,
|
|
603
|
+
SkillPrimitive,
|
|
604
|
+
PlaybookPrimitive,
|
|
605
|
+
AgentPrimitive,
|
|
606
|
+
PosturePrimitive,
|
|
607
|
+
CorpusPrimitive,
|
|
608
|
+
MixinPrimitive
|
|
609
|
+
];
|
|
610
|
+
var KINDS = Object.freeze(PRIMITIVE_CLASSES.map((one) => one.kind));
|
|
611
|
+
var PRIMITIVE_IDENTITY_SCHEMA = z4.string().regex(/^[a-z0-9-]+:[a-z0-9-]+$/).transform((identity) => identity);
|
|
612
|
+
function identityOf(kindAndId) {
|
|
613
|
+
if (typeof kindAndId !== "string") return `${kindAndId.kind}:${kindAndId.id}`;
|
|
614
|
+
const parsed = PRIMITIVE_IDENTITY_SCHEMA.safeParse(kindAndId);
|
|
615
|
+
if (!parsed.success)
|
|
616
|
+
throw new CharterPrimitiveFault(
|
|
617
|
+
`"${kindAndId}" is not an identity: one is a kind and an id with a colon between them.`,
|
|
618
|
+
'Write it as <kind>:<id>, as in "guide:no-any".'
|
|
619
|
+
);
|
|
620
|
+
return parsed.data;
|
|
621
|
+
}
|
|
622
|
+
function isKind(value) {
|
|
623
|
+
return typeof value === "string" && KINDS.includes(value);
|
|
624
|
+
}
|
|
625
|
+
var CLASS_OF = new Map(
|
|
626
|
+
PRIMITIVE_CLASSES.map((one) => [one.kind, one])
|
|
627
|
+
);
|
|
628
|
+
var PrimitiveHeader = class {
|
|
629
|
+
constructor(field, shape, required, allowedValues) {
|
|
630
|
+
this.field = field;
|
|
631
|
+
this.shape = shape;
|
|
632
|
+
this.required = required;
|
|
633
|
+
this.allowedValues = allowedValues;
|
|
634
|
+
}
|
|
635
|
+
field;
|
|
636
|
+
shape;
|
|
637
|
+
required;
|
|
638
|
+
allowedValues;
|
|
639
|
+
};
|
|
640
|
+
var PrimitiveRequirements = class {
|
|
641
|
+
constructor(headers, sample) {
|
|
642
|
+
this.headers = headers;
|
|
643
|
+
this.sample = sample;
|
|
644
|
+
}
|
|
645
|
+
headers;
|
|
646
|
+
sample;
|
|
647
|
+
};
|
|
648
|
+
function primitiveHeadersOf(kind) {
|
|
649
|
+
const kindClass = CLASS_OF.get(kind);
|
|
650
|
+
const required = { ...BasePrimitive.requires, ...kindClass.requires };
|
|
651
|
+
return Object.entries(kindClass.schema.shape).filter(([field]) => field !== "id").map(([field, type]) => {
|
|
652
|
+
let heldType = type;
|
|
653
|
+
while (!(heldType instanceof z4.ZodArray) && typeof heldType?.unwrap === "function")
|
|
654
|
+
heldType = heldType.unwrap();
|
|
655
|
+
return new PrimitiveHeader(
|
|
656
|
+
field,
|
|
657
|
+
heldType instanceof z4.ZodArray ? "list" : "line",
|
|
658
|
+
field in required,
|
|
659
|
+
heldType instanceof z4.ZodEnum ? heldType.options : void 0
|
|
660
|
+
);
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
function primitiveSampleOf(kind) {
|
|
664
|
+
return Object.entries({ kind, ...CLASS_OF.get(kind).sample }).map(([field, value]) => `${field}: ${formatFrontmatterValue(value)}`).join("\n");
|
|
665
|
+
}
|
|
666
|
+
function primitiveOf(input, parser) {
|
|
667
|
+
let headers;
|
|
668
|
+
let body;
|
|
669
|
+
if (typeof input === "string") {
|
|
670
|
+
const lines3 = input.replace(/^/, "").split(/\r?\n/);
|
|
671
|
+
if (lines3[0]?.trim() !== DELIMITER)
|
|
672
|
+
throwAggregateError([
|
|
673
|
+
new CharterPrimitiveFault(
|
|
674
|
+
`This file opens with no "${DELIMITER}" frontmatter block.`,
|
|
675
|
+
`Add one at the top declaring at least "id" and "description".`
|
|
676
|
+
)
|
|
677
|
+
]);
|
|
678
|
+
const end = lines3.findIndex((line, index) => index > 0 && line.trim() === DELIMITER);
|
|
679
|
+
if (end === -1)
|
|
680
|
+
throwAggregateError([
|
|
681
|
+
new CharterPrimitiveFault(
|
|
682
|
+
`The frontmatter block is never closed.`,
|
|
683
|
+
`Add a "${DELIMITER}" line after the headers.`
|
|
684
|
+
)
|
|
685
|
+
]);
|
|
686
|
+
headers = parser.parse(lines3.slice(1, end).join("\n"));
|
|
687
|
+
body = lines3.slice(end + 1).join("\n").trim();
|
|
688
|
+
} else {
|
|
689
|
+
headers = input.headers;
|
|
690
|
+
body = input.body;
|
|
691
|
+
}
|
|
692
|
+
if (!isKind(headers.kind))
|
|
693
|
+
throwAggregateError([
|
|
694
|
+
headers.kind === void 0 ? new CharterPrimitiveFault(
|
|
695
|
+
`This file declares no "kind", so nothing knows what to read it as.`,
|
|
696
|
+
`Add "kind: <one the charter knows>" to the frontmatter.`
|
|
697
|
+
) : new CharterPrimitiveFault(
|
|
698
|
+
`"${String(headers.kind)}" is not a kind the charter knows.`,
|
|
699
|
+
`Declare a kind the charter knows: ${KINDS.join(", ")}.`
|
|
700
|
+
)
|
|
701
|
+
]);
|
|
702
|
+
const kind = headers.kind;
|
|
703
|
+
if (typeof input === "string") return CLASS_OF.get(kind).of(headers, body);
|
|
704
|
+
const headerFields = primitiveHeadersOf(kind).map((header) => header.field);
|
|
705
|
+
const unknownHeaderFaults = Object.keys(headers).filter((field) => field !== "kind" && field !== "id" && !headerFields.includes(field)).map(
|
|
706
|
+
(field) => new CharterPrimitiveFault(
|
|
707
|
+
`A ${kind} holds no "${field}".`,
|
|
708
|
+
`Answer one of what a ${kind} holds: ${headerFields.join(", ")}.`
|
|
709
|
+
)
|
|
710
|
+
);
|
|
711
|
+
let primitive;
|
|
712
|
+
let schemaFaults = [];
|
|
713
|
+
try {
|
|
714
|
+
primitive = CLASS_OF.get(kind).of(headers, body);
|
|
715
|
+
} catch (raised) {
|
|
716
|
+
if (!(raised instanceof AggregateError)) throw raised;
|
|
717
|
+
schemaFaults = raised.errors;
|
|
718
|
+
}
|
|
719
|
+
if (primitive === void 0 || unknownHeaderFaults.length > 0) throwAggregateError([...unknownHeaderFaults, ...schemaFaults]);
|
|
720
|
+
return primitive;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// src/hexagon/domain/models/charter/CharterRoot.ts
|
|
724
|
+
var REPO_SCOPE = "repo";
|
|
725
|
+
var VENDOR_SCOPE = "vendor";
|
|
726
|
+
var BUILTIN_SCOPE = "builtin";
|
|
727
|
+
var BUILTIN_PATH_PREFIX = "(built into cw)";
|
|
728
|
+
var ScopedPrimitive = class {
|
|
729
|
+
constructor(identity, scope, file, primitive) {
|
|
730
|
+
this.identity = identity;
|
|
731
|
+
this.scope = scope;
|
|
732
|
+
this.file = file;
|
|
733
|
+
this.primitive = primitive;
|
|
734
|
+
}
|
|
735
|
+
identity;
|
|
736
|
+
scope;
|
|
737
|
+
file;
|
|
738
|
+
primitive;
|
|
739
|
+
/** Whether a word someone searched for is anywhere a reader would look for
|
|
740
|
+
* it: the identity, the kind, what it is for, when its kind comes up, its
|
|
741
|
+
* file, or any header it declared — ignoring case, since whoever typed it
|
|
742
|
+
* did not know how it was written (FR-114). */
|
|
743
|
+
mentions(word) {
|
|
744
|
+
const activatesWhen = PRIMITIVE_CLASSES.find((one) => one.kind === this.primitive.kind).activatesWhen;
|
|
745
|
+
const headerValues = Object.values(this.primitive.headers).flatMap(
|
|
746
|
+
(value) => value === void 0 ? [] : Array.isArray(value) ? value : [String(value)]
|
|
747
|
+
);
|
|
748
|
+
const loweredWord = word.toLowerCase();
|
|
749
|
+
return [this.identity, this.primitive.kind, activatesWhen, this.file, ...headerValues].some(
|
|
750
|
+
(one) => one.toLowerCase().includes(loweredWord)
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
};
|
|
754
|
+
var CharterRoot = class {
|
|
755
|
+
constructor(primitives, faultsByFiles) {
|
|
756
|
+
this.primitives = primitives;
|
|
757
|
+
this.faultsByFiles = faultsByFiles;
|
|
758
|
+
}
|
|
759
|
+
primitives;
|
|
760
|
+
faultsByFiles;
|
|
761
|
+
/** Everything wrong with this charter, under the file that has to change: the
|
|
762
|
+
* files that could not be read as a primitive at all, and what the ones that
|
|
763
|
+
* were read get wrong together. The two never name one file — a file reading
|
|
764
|
+
* refused holds no primitive to get anything else wrong — so one asking is
|
|
765
|
+
* the whole answer (FR-009). */
|
|
766
|
+
get allFaultsByFiles() {
|
|
767
|
+
return this.faultsByFiles.with(this.compositeFaultsByFiles);
|
|
768
|
+
}
|
|
769
|
+
/** Every mixin, under the id a host names it by, wherever it was authored. */
|
|
770
|
+
get mixins() {
|
|
771
|
+
return new Map(
|
|
772
|
+
this.primitives.filter(({ primitive }) => primitive.kind === "mixin").map((one) => [one.primitive.headers.id, one.primitive])
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* What one primitive says once its mixins are in it: the body of every mixin
|
|
777
|
+
* it pulls in, in the order it named them, and then its own (FR-006).
|
|
778
|
+
*
|
|
779
|
+
* The one place a mixin is applied at all. Nothing is merged and no header
|
|
780
|
+
* moves — a mixin lends text, so what comes back is text, and the host's own
|
|
781
|
+
* words come last so they read as the point and the lent ones as the setting.
|
|
782
|
+
*
|
|
783
|
+
* Asked of the charter because the charter is what already read the mixins
|
|
784
|
+
* and knows which identity a name means across layers. A mixin nothing
|
|
785
|
+
* answers to is left out rather than raised: `allFaultsByFiles` already names
|
|
786
|
+
* that file, and a build refused for it never reaches here (FR-009).
|
|
787
|
+
*/
|
|
788
|
+
bodyOf(one) {
|
|
789
|
+
const mixins = this.mixins;
|
|
790
|
+
return [
|
|
791
|
+
...(one.primitive.headers.mixins ?? []).map((named) => mixins.get(named)?.body),
|
|
792
|
+
one.primitive.body
|
|
793
|
+
].filter((body) => body !== void 0 && body.trim() !== "").join("\n\n");
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Every primitive under the identity the whole charter names it by (FR-017).
|
|
797
|
+
*
|
|
798
|
+
* Held here for the reason `mixins` is: one identity space covers every
|
|
799
|
+
* layer, so which file declares a name is a question about the whole charter
|
|
800
|
+
* and not about either layer on its own (FR-014).
|
|
801
|
+
*
|
|
802
|
+
* The first claim on an identity is the one in here where two files claim it.
|
|
803
|
+
* A collision is what `compositeFaultsByFiles` is for, and it names both files
|
|
804
|
+
* there; nothing is asked to explain a charter that has one (SC-006).
|
|
805
|
+
*/
|
|
806
|
+
get primitiveById() {
|
|
807
|
+
const byIdentity = /* @__PURE__ */ new Map();
|
|
808
|
+
for (const one of this.primitives) if (!byIdentity.has(one.identity)) byIdentity.set(one.identity, one);
|
|
809
|
+
return byIdentity;
|
|
810
|
+
}
|
|
811
|
+
/** Every corpus, under the identity a primitive cites it by, wherever it was
|
|
812
|
+
* authored: `corpus:<id>`. */
|
|
813
|
+
get corpora() {
|
|
814
|
+
return new Set(
|
|
815
|
+
this.primitives.filter(({ primitive }) => primitive.kind === "corpus").map((one) => one.identity)
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
/** Every mixin one primitive uses, in the order it named them (FR-014). A
|
|
819
|
+
* name nothing answers to is left out: `compositeFaultsByFiles` already
|
|
820
|
+
* names that file as an error. */
|
|
821
|
+
mixinsOf(one) {
|
|
822
|
+
const byIdentity = this.primitiveById;
|
|
823
|
+
return (one.primitive.headers.mixins ?? []).flatMap((id) => byIdentity.get(`mixin:${id}`) ?? []);
|
|
824
|
+
}
|
|
825
|
+
/** The corpus one primitive cites as its rationale, or nothing where it cites
|
|
826
|
+
* none or cites one this charter does not hold — the second is a warning
|
|
827
|
+
* `compositeFaultsByFiles` already names (FR-005, FR-014). */
|
|
828
|
+
rationaleOf(one) {
|
|
829
|
+
const { rationale } = one.primitive.headers;
|
|
830
|
+
return rationale !== void 0 && this.corpora.has(rationale) ? this.primitiveById.get(rationale) : void 0;
|
|
831
|
+
}
|
|
832
|
+
/** Every primitive using one mixin; nothing for any other kind (FR-014). */
|
|
833
|
+
hostsOf(mixin) {
|
|
834
|
+
if (mixin.primitive.kind !== "mixin") return [];
|
|
835
|
+
return this.primitives.filter((host) => (host.primitive.headers.mixins ?? []).includes(mixin.primitive.headers.id));
|
|
836
|
+
}
|
|
837
|
+
/** Every primitive citing one corpus as its rationale; nothing for any other
|
|
838
|
+
* kind (FR-014). */
|
|
839
|
+
citersOf(corpus) {
|
|
840
|
+
if (corpus.primitive.kind !== "corpus") return [];
|
|
841
|
+
return this.primitives.filter((citer) => citer.primitive.headers.rationale === corpus.identity);
|
|
842
|
+
}
|
|
843
|
+
/** Does what a mixin lends reach as far as the host pulling it in, or the host
|
|
844
|
+
* no further than the mixin? A side that names no files is asking about
|
|
845
|
+
* nothing in particular, and is left alone (FR-006). */
|
|
846
|
+
reaches(host, mixin) {
|
|
847
|
+
const hostGlobs = host.headers.globs ?? [];
|
|
848
|
+
const mixinGlobs = mixin.headers.globs ?? [];
|
|
849
|
+
if (hostGlobs.length === 0 || mixinGlobs.length === 0) return true;
|
|
850
|
+
const coversAll = (outer, inner) => inner.every((narrower) => outer.some((wider) => covers(wider, narrower)));
|
|
851
|
+
return coversAll(mixinGlobs, hostGlobs) || coversAll(hostGlobs, mixinGlobs);
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* What is wrong with this charter that is wrong with no one file on its own,
|
|
855
|
+
* under the file that has to change (FR-009).
|
|
856
|
+
*
|
|
857
|
+
* Reading refused the files whose own headers do not hold, and said so where
|
|
858
|
+
* it read them; what is left are the questions no single file answers. Every
|
|
859
|
+
* layer is read into one identity space, the way one registry holds one name
|
|
860
|
+
* per package: two files claim one identity; one names a mixin nothing
|
|
861
|
+
* answers to; one pulls in a mixin that speaks about other files than it does
|
|
862
|
+
* — a mixin lends its body at projection time, so what makes the two belong
|
|
863
|
+
* together is the files they both apply to (FR-006, FR-014). One cites a
|
|
864
|
+
* rationale no corpus answers to, which is a warning rather than an error:
|
|
865
|
+
* the rule holds without its reasoning, and the author is told the reasoning
|
|
866
|
+
* is gone (FR-005). A corpus nobody cites and a mixin nobody pulls in are
|
|
867
|
+
* warnings too: nothing breaks, and nothing reads them either (FR-014).
|
|
868
|
+
*/
|
|
869
|
+
get compositeFaultsByFiles() {
|
|
870
|
+
const everyPrimitive = this.primitives;
|
|
871
|
+
const mixinsByIdentity = this.mixins;
|
|
872
|
+
const corpusIdentities = this.corpora;
|
|
873
|
+
const fileByIdentity = /* @__PURE__ */ new Map();
|
|
874
|
+
const faultsByFiles = {};
|
|
875
|
+
const addFault = (file, found) => {
|
|
876
|
+
faultsByFiles[file] = [...faultsByFiles[file] ?? [], found];
|
|
877
|
+
};
|
|
878
|
+
for (const one of everyPrimitive) {
|
|
879
|
+
const { file, primitive } = one;
|
|
880
|
+
const { identity } = one;
|
|
881
|
+
const declaredIn = fileByIdentity.get(identity);
|
|
882
|
+
if (declaredIn === void 0) fileByIdentity.set(identity, file);
|
|
883
|
+
else
|
|
884
|
+
addFault(
|
|
885
|
+
file,
|
|
886
|
+
new CharterRootFault(
|
|
887
|
+
`"${identity}" is already declared by ${declaredIn}. One identity names one primitive in a charter, whichever layer it was authored in.`,
|
|
888
|
+
`Give this one an id of its own, or delete it if the other says the same thing. A vendor claiming an id you authored is one to raise with whoever publishes it. An id ${BUILTIN_PATH_PREFIX} claims is the engine's own and nobody can change it, so the one to rename is yours.`
|
|
889
|
+
)
|
|
890
|
+
);
|
|
891
|
+
for (const namedMixin of primitive.headers.mixins ?? []) {
|
|
892
|
+
const mixin = mixinsByIdentity.get(namedMixin);
|
|
893
|
+
if (mixin === void 0) {
|
|
894
|
+
addFault(
|
|
895
|
+
file,
|
|
896
|
+
new CharterRootFault(
|
|
897
|
+
`This pulls in the mixin "${namedMixin}", and this charter holds no mixin of that id.`,
|
|
898
|
+
`Author it, or drop "${namedMixin}" from "mixins". A mixin is named by its id, whichever layer authored it.`
|
|
899
|
+
)
|
|
900
|
+
);
|
|
901
|
+
continue;
|
|
902
|
+
}
|
|
903
|
+
if (!this.reaches(primitive, mixin)) {
|
|
904
|
+
addFault(
|
|
905
|
+
file,
|
|
906
|
+
new CharterRootFault(
|
|
907
|
+
`This pulls in the mixin "${namedMixin}", and neither one's "globs" covers the other's. The mixin's body would be written into a primitive that applies to different files.`,
|
|
908
|
+
`Widen one side's "globs" until it covers the other's, or move the shared text into a corpus and cite it with "rationale".`
|
|
909
|
+
)
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
const cited = primitive.headers.rationale;
|
|
914
|
+
if (cited !== void 0 && !corpusIdentities.has(cited)) {
|
|
915
|
+
addFault(
|
|
916
|
+
file,
|
|
917
|
+
new CharterRootFault(
|
|
918
|
+
`This cites "${cited}" as its rationale, and this charter holds no corpus of that identity.`,
|
|
919
|
+
`Author that corpus, correct the reference, or drop "rationale". A corpus is cited as "corpus:<id>", whichever layer authored it.`,
|
|
920
|
+
"warn"
|
|
921
|
+
)
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
if (primitive.kind === "corpus" && this.citersOf(one).length === 0)
|
|
925
|
+
addFault(
|
|
926
|
+
file,
|
|
927
|
+
new CharterRootFault(
|
|
928
|
+
`No primitive cites "${identity}" as its rationale, so nothing an agent reads leads to it.`,
|
|
929
|
+
`Cite it from the rules it explains with "rationale: ${identity}", or delete it.`,
|
|
930
|
+
"warn"
|
|
931
|
+
)
|
|
932
|
+
);
|
|
933
|
+
if (primitive.kind === "mixin" && this.hostsOf(one).length === 0)
|
|
934
|
+
addFault(
|
|
935
|
+
file,
|
|
936
|
+
new CharterRootFault(
|
|
937
|
+
`No primitive pulls in the mixin "${primitive.headers.id}", so its body is never written anywhere.`,
|
|
938
|
+
`Name "${primitive.headers.id}" under "mixins" of the primitives it was written for, or delete it.`,
|
|
939
|
+
"warn"
|
|
940
|
+
)
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
return new FaultsByFile(faultsByFiles);
|
|
944
|
+
}
|
|
945
|
+
};
|
|
946
|
+
function charterRootOf({
|
|
947
|
+
repo,
|
|
948
|
+
vendor,
|
|
949
|
+
builtin
|
|
950
|
+
}, yamlParser2) {
|
|
951
|
+
const primitives = [];
|
|
952
|
+
const faultsByFiles = {};
|
|
953
|
+
const read = (one, scope) => {
|
|
954
|
+
try {
|
|
955
|
+
const primitive = primitiveOf(one.contents, yamlParser2);
|
|
956
|
+
primitives.push(new ScopedPrimitive(identityOf({ kind: primitive.kind, id: primitive.headers.id }), scope, one.path, primitive));
|
|
957
|
+
} catch (raised) {
|
|
958
|
+
if (!(raised instanceof AggregateError)) throw raised;
|
|
959
|
+
faultsByFiles[one.path] = raised.errors;
|
|
960
|
+
}
|
|
961
|
+
};
|
|
962
|
+
const sorted = (files) => [...files].sort((one, another) => one.path.localeCompare(another.path));
|
|
963
|
+
for (const one of sorted(builtin)) read(one, BUILTIN_SCOPE);
|
|
964
|
+
for (const one of sorted(repo)) read(one, REPO_SCOPE);
|
|
965
|
+
for (const one of sorted(vendor)) read(one, VENDOR_SCOPE);
|
|
966
|
+
return new CharterRoot(primitives, new FaultsByFile(faultsByFiles));
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// src/hexagon/domain/path.ts
|
|
970
|
+
function folderURL(at) {
|
|
971
|
+
const url = new URL(at);
|
|
972
|
+
if (!url.pathname.endsWith("/"))
|
|
973
|
+
throw new Fault(
|
|
974
|
+
`"${url.href}" names a file, and a folder is what is wanted here.`,
|
|
975
|
+
`Write it with the separator a folder ends in, as in "${url.href}/".`
|
|
976
|
+
);
|
|
977
|
+
return url;
|
|
978
|
+
}
|
|
979
|
+
var WORKSPACE_DIRECTORY = ".cw";
|
|
980
|
+
var CHARTER_DIRECTORY = `${WORKSPACE_DIRECTORY}/charter`;
|
|
981
|
+
var VENDOR_DIRECTORY = `${WORKSPACE_DIRECTORY}/vendor`;
|
|
982
|
+
var TEST_DIRECTORY = `${WORKSPACE_DIRECTORY}/test`;
|
|
983
|
+
var OUT_DIRECTORY = `${WORKSPACE_DIRECTORY}/out`;
|
|
984
|
+
var SETTINGS_FILE = `${WORKSPACE_DIRECTORY}/settings.json`;
|
|
985
|
+
function charterFolderIn(repo) {
|
|
986
|
+
return folderIn(repo, CHARTER_DIRECTORY);
|
|
987
|
+
}
|
|
988
|
+
function outFolderIn(repo) {
|
|
989
|
+
return folderIn(repo, OUT_DIRECTORY);
|
|
990
|
+
}
|
|
991
|
+
var CLAUDE_DIRECTORY = ".claude";
|
|
992
|
+
var CLAUDE_ENTRY_FILE = "CLAUDE.md";
|
|
993
|
+
function agentProviderFolderIn(repo, provider) {
|
|
994
|
+
switch (provider) {
|
|
995
|
+
case "claude":
|
|
996
|
+
return folderIn(repo, CLAUDE_DIRECTORY);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
function testFolderIn(repo) {
|
|
1000
|
+
return folderIn(repo, TEST_DIRECTORY);
|
|
1001
|
+
}
|
|
1002
|
+
function vendorFolderIn(repo) {
|
|
1003
|
+
return folderIn(repo, VENDOR_DIRECTORY);
|
|
1004
|
+
}
|
|
1005
|
+
function folderIn(repo, directory) {
|
|
1006
|
+
return folderURL(new URL(`${directory}/`, repo.href.endsWith("/") ? repo : new URL(`${repo.href}/`)));
|
|
1007
|
+
}
|
|
1008
|
+
function settingsFileIn(repo) {
|
|
1009
|
+
return new URL(SETTINGS_FILE, repo.href.endsWith("/") ? repo : new URL(`${repo.href}/`));
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// src/driver/cli/commands/AddCommand.ts
|
|
1013
|
+
import prompts from "prompts";
|
|
1014
|
+
|
|
1015
|
+
// src/driver/cli/commands/Command.ts
|
|
1016
|
+
import { pathToFileURL } from "url";
|
|
1017
|
+
var EXIT_OK = 0;
|
|
1018
|
+
var EXIT_FAILURE = 1;
|
|
1019
|
+
var EXIT_USAGE = 2;
|
|
1020
|
+
|
|
1021
|
+
// src/driver/cli/commands/helper.ts
|
|
1022
|
+
import { spawn } from "child_process";
|
|
1023
|
+
function parseKVParams(typed, paramName) {
|
|
1024
|
+
const params = /* @__PURE__ */ Object.create(null);
|
|
1025
|
+
for (const one of typed) {
|
|
1026
|
+
const at = one.indexOf("=");
|
|
1027
|
+
if (at <= 0)
|
|
1028
|
+
throw new Fault(
|
|
1029
|
+
`"--${paramName} ${one}" names nothing to put a value under.`,
|
|
1030
|
+
`Type each one as --${paramName} name=value, once per name.`
|
|
1031
|
+
);
|
|
1032
|
+
(params[one.slice(0, at)] ??= []).push(one.slice(at + 1));
|
|
1033
|
+
}
|
|
1034
|
+
return params;
|
|
1035
|
+
}
|
|
1036
|
+
function toText({ data: { files } }) {
|
|
1037
|
+
return Object.keys(files).sort().map((file) => [file, ...(files[file] ?? []).flatMap(lines), ""].join("\n")).join("");
|
|
1038
|
+
}
|
|
1039
|
+
function lines({ data: { severity, message, fix } }) {
|
|
1040
|
+
return [` ${severity}: ${message}`, ...fix ? [` ${fix}`] : []];
|
|
1041
|
+
}
|
|
1042
|
+
async function editInEditor(cwd, file) {
|
|
1043
|
+
const editorCommand = process.env.VISUAL || process.env.EDITOR;
|
|
1044
|
+
if (!editorCommand)
|
|
1045
|
+
throw new Fault("Neither $VISUAL nor $EDITOR is set, so there is no editor to open it in.", 'Set one, as in "export EDITOR=vim".');
|
|
1046
|
+
const exitCode = await new Promise(
|
|
1047
|
+
(resolve, reject) => spawn("sh", ["-c", `${editorCommand} "$1"`, editorCommand, `${cwd}/${file}`], { stdio: "inherit" }).once("error", reject).once("exit", resolve)
|
|
1048
|
+
);
|
|
1049
|
+
return exitCode === 0 ? void 0 : `${editorCommand} exited with ${exitCode} on ${file}.
|
|
1050
|
+
`;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// src/driver/cli/commands/AddCommand.ts
|
|
1054
|
+
var OPTIONS = {
|
|
1055
|
+
kind: {
|
|
1056
|
+
type: "string",
|
|
1057
|
+
describe: "Which kind of primitive to write; a word that is none is answered with every kind there is"
|
|
1058
|
+
},
|
|
1059
|
+
id: {
|
|
1060
|
+
type: "string",
|
|
1061
|
+
describe: 'What it is named by across the whole charter: "no-any"'
|
|
1062
|
+
},
|
|
1063
|
+
header: {
|
|
1064
|
+
type: "string",
|
|
1065
|
+
isArray: true,
|
|
1066
|
+
describe: 'One header the kind requires, as name=value; typed once per header, and what "cw kinds <kind>" names'
|
|
1067
|
+
}
|
|
1068
|
+
};
|
|
1069
|
+
var AddCommand = class {
|
|
1070
|
+
name = "add <kind> <id>";
|
|
1071
|
+
summary = "Write a new primitive of one kind, holding what that kind requires";
|
|
1072
|
+
options = OPTIONS;
|
|
1073
|
+
async run({ charterAuthoringApp: charterAuthoringApp2 }, { kind, id, header }) {
|
|
1074
|
+
const taken = (await charterAuthoringApp2.listPrimitiveRequirements(kind ?? "")).data.headers.map((one) => one.data);
|
|
1075
|
+
const lists = new Set(taken.filter((one) => one.shape === "list").map((one) => one.field));
|
|
1076
|
+
const answers = header.length > 0 ? Object.fromEntries(
|
|
1077
|
+
Object.entries(parseKVParams(header, "header")).map(([field, values]) => {
|
|
1078
|
+
if (lists.has(field)) return [field, values];
|
|
1079
|
+
if (values.length > 1)
|
|
1080
|
+
throw new Fault(
|
|
1081
|
+
`"${field}" holds one line, and was typed ${values.length} times.`,
|
|
1082
|
+
`Type "--header ${field}=..." once.`
|
|
1083
|
+
);
|
|
1084
|
+
return [field, values[0]];
|
|
1085
|
+
})
|
|
1086
|
+
) : await this.#answered(
|
|
1087
|
+
taken.filter((one) => one.required),
|
|
1088
|
+
kind ?? ""
|
|
1089
|
+
);
|
|
1090
|
+
const scopedPrimitiveDTO2 = await charterAuthoringApp2.add(kind ?? "", id ?? "", answers);
|
|
1091
|
+
if (scopedPrimitiveDTO2.type === "Faults")
|
|
1092
|
+
return {
|
|
1093
|
+
code: EXIT_FAILURE,
|
|
1094
|
+
problem: scopedPrimitiveDTO2.data.faults.map(({ data: { message, fix } }) => `${message}
|
|
1095
|
+
${fix}
|
|
1096
|
+
`).join("")
|
|
1097
|
+
};
|
|
1098
|
+
return {
|
|
1099
|
+
code: EXIT_OK,
|
|
1100
|
+
result: [`Wrote ${scopedPrimitiveDTO2.data.file}.`, 'Write what it has to say in the body, then run "cw build".', ""].join("\n")
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* Every header this kind requires, answered by whoever is at the terminal.
|
|
1105
|
+
*
|
|
1106
|
+
* One question per header, in the order the engine named them, each asked as
|
|
1107
|
+
* what it holds: one line of text, or a list of names typed with commas between
|
|
1108
|
+
* them.
|
|
1109
|
+
*
|
|
1110
|
+
* Nobody at the terminal is refused rather than answered with blanks: every
|
|
1111
|
+
* header here is one its kind requires, so a file written without them is a
|
|
1112
|
+
* file the next build refuses, and writing it would leave the author to find
|
|
1113
|
+
* that out later (FR-035 is setup's, and this asks for what nothing can default).
|
|
1114
|
+
*/
|
|
1115
|
+
async #answered(required, kind) {
|
|
1116
|
+
if (!process.stdin.isTTY)
|
|
1117
|
+
throw new Fault(
|
|
1118
|
+
`A ${kind} requires ${required.map((one) => `"${one.field}"`).join(", ")}, and nobody is at the terminal to answer.`,
|
|
1119
|
+
`Run "cw add" where you can answer, or author the file under the charter yourself.`
|
|
1120
|
+
);
|
|
1121
|
+
const answers = await prompts(
|
|
1122
|
+
required.map(
|
|
1123
|
+
({ field, shape }) => shape === "line" ? { type: "text", name: field, message: `${field}?` } : {
|
|
1124
|
+
type: "list",
|
|
1125
|
+
name: field,
|
|
1126
|
+
message: `${field}?`,
|
|
1127
|
+
// `prompts` hands an answer left empty back as [""].
|
|
1128
|
+
format: (entries) => entries.map((one) => one.trim()).filter((one) => one !== "")
|
|
1129
|
+
}
|
|
1130
|
+
)
|
|
1131
|
+
);
|
|
1132
|
+
if (required.some((one) => answers[one.field] === void 0))
|
|
1133
|
+
throw new Fault("This was stopped before it had its answers, so nothing was written.", 'Run "cw add" again.');
|
|
1134
|
+
return answers;
|
|
1135
|
+
}
|
|
1136
|
+
};
|
|
1137
|
+
|
|
1138
|
+
// src/driver/cli/commands/BuildCommand.ts
|
|
1139
|
+
var BUILDING = {
|
|
1140
|
+
preview: {
|
|
1141
|
+
type: "boolean",
|
|
1142
|
+
describe: "Say what a build would do to each file, and write nothing",
|
|
1143
|
+
default: false
|
|
1144
|
+
}
|
|
1145
|
+
};
|
|
1146
|
+
var BuildCommand = class {
|
|
1147
|
+
name = "build";
|
|
1148
|
+
summary = "Compile the charter into the files each agent reads";
|
|
1149
|
+
options = BUILDING;
|
|
1150
|
+
async run({ charterAuthoringApp: charterAuthoringApp2 }, { preview }) {
|
|
1151
|
+
if (preview) {
|
|
1152
|
+
const planSummaryDTO3 = await charterAuthoringApp2.preview();
|
|
1153
|
+
if (planSummaryDTO3.type === "FaultsByFile") {
|
|
1154
|
+
const noOfFiles = Object.keys(planSummaryDTO3.data.files).length;
|
|
1155
|
+
return {
|
|
1156
|
+
code: EXIT_FAILURE,
|
|
1157
|
+
problem: `Nothing was previewed: ${noOfFiles} file${noOfFiles === 1 ? " has" : "s have"} errors.
|
|
1158
|
+
Run "cw doctor" to see what is wrong with them.
|
|
1159
|
+
`
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
const { added: added2, edited: edited2, deleted: deleted2, unchanged } = planSummaryDTO3.data;
|
|
1163
|
+
const changes = [
|
|
1164
|
+
...added2.map((path) => ["create", path]),
|
|
1165
|
+
...edited2.map((path) => ["update", path]),
|
|
1166
|
+
...deleted2.map((path) => ["delete", path]),
|
|
1167
|
+
...unchanged.map((path) => ["unchanged", path])
|
|
1168
|
+
];
|
|
1169
|
+
return {
|
|
1170
|
+
code: EXIT_OK,
|
|
1171
|
+
result: [
|
|
1172
|
+
...changes.map(([change, path]) => ` ${change.padEnd("unchanged".length)} ${path}`),
|
|
1173
|
+
`${changes.length} file${changes.length === 1 ? "" : "s"}: ${added2.length} create, ${edited2.length} update, ${deleted2.length} delete, ${unchanged.length} unchanged.`,
|
|
1174
|
+
""
|
|
1175
|
+
].join("\n")
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
const planSummaryDTO2 = await charterAuthoringApp2.build();
|
|
1179
|
+
if (planSummaryDTO2.type === "FaultsByFile") {
|
|
1180
|
+
const noOfFiles = Object.keys(planSummaryDTO2.data.files).length;
|
|
1181
|
+
return {
|
|
1182
|
+
code: EXIT_FAILURE,
|
|
1183
|
+
problem: `Nothing was built: ${noOfFiles} file${noOfFiles === 1 ? " has" : "s have"} errors.
|
|
1184
|
+
Run "cw doctor" to see what is wrong with them.
|
|
1185
|
+
`
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
const { added, edited, deleted } = planSummaryDTO2.data;
|
|
1189
|
+
const wrote = added.length + edited.length;
|
|
1190
|
+
return {
|
|
1191
|
+
code: EXIT_OK,
|
|
1192
|
+
result: [
|
|
1193
|
+
`Built ${wrote} file${wrote === 1 ? "" : "s"}: ${added.length} added, ${edited.length} changed.`,
|
|
1194
|
+
...deleted.map((path) => ` deleted ${path}, whose primitive is gone`),
|
|
1195
|
+
""
|
|
1196
|
+
].join("\n")
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
};
|
|
1200
|
+
|
|
1201
|
+
// src/driver/cli/commands/DoctorCommand.ts
|
|
1202
|
+
var DoctorCommand = class {
|
|
1203
|
+
name = "doctor";
|
|
1204
|
+
summary = "Check this repository: agents, charter, vendors, and what is built";
|
|
1205
|
+
async run({ version, charterAuthoringApp: charterAuthoringApp2 }, _options) {
|
|
1206
|
+
const {
|
|
1207
|
+
agents,
|
|
1208
|
+
errorCount: errors,
|
|
1209
|
+
warnCount: warnings,
|
|
1210
|
+
pendingCount: pending,
|
|
1211
|
+
faultsByFile: faultsByFile2,
|
|
1212
|
+
driftedVendors: driftedVendors2,
|
|
1213
|
+
problemCount
|
|
1214
|
+
} = (await charterAuthoringApp2.doctor()).data;
|
|
1215
|
+
return {
|
|
1216
|
+
code: problemCount === 0 ? EXIT_OK : EXIT_FAILURE,
|
|
1217
|
+
result: [
|
|
1218
|
+
// Which cw wrote the report, so one pasted into a bug report names
|
|
1219
|
+
// the release it came from (FR-132).
|
|
1220
|
+
`cw ${version}`,
|
|
1221
|
+
// A repository that compiles for none is set up and building: the
|
|
1222
|
+
// neutral surface is compiled for everybody (FR-019).
|
|
1223
|
+
agents.length === 0 ? "Agents: none chosen, so only the neutral surface is compiled." : `Agents: ${agents.join(", ")}.`,
|
|
1224
|
+
errors > 0 ? `Charter: ${errors} error${errors === 1 ? "" : "s"}.` : warnings === 0 ? "Charter: holds." : `Charter: holds, with ${warnings} warning${warnings === 1 ? "" : "s"}.`,
|
|
1225
|
+
driftedVendors2.length === 0 ? "Vendors: none edited here." : `Vendors: edited here: ${driftedVendors2.join(", ")}. Run "git checkout" under .cw/vendor/ to undo, or commit what you meant.`,
|
|
1226
|
+
pending === null ? "Built: not known, since the charter does not hold." : pending === 0 ? "Built: up to date." : `Built: ${pending} file${pending === 1 ? "" : "s"} out of date. Run "cw build".`,
|
|
1227
|
+
// Every fault under the file that has to change, below the four lines
|
|
1228
|
+
// rather than inside them: a line says how many, and this says which
|
|
1229
|
+
// (FR-009).
|
|
1230
|
+
...Object.keys(faultsByFile2.data.files).length === 0 ? [] : ["", toText(faultsByFile2)],
|
|
1231
|
+
problemCount === 0 ? "Nothing to fix." : `${problemCount} thing${problemCount === 1 ? "" : "s"} to fix.`,
|
|
1232
|
+
""
|
|
1233
|
+
].join("\n")
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
};
|
|
1237
|
+
|
|
1238
|
+
// src/driver/cli/commands/EditCommand.ts
|
|
1239
|
+
var OPTIONS2 = {
|
|
1240
|
+
identity: {
|
|
1241
|
+
type: "string",
|
|
1242
|
+
describe: 'What the primitive is named by across the whole charter: "guide:no-any"'
|
|
1243
|
+
}
|
|
1244
|
+
};
|
|
1245
|
+
var EditCommand = class {
|
|
1246
|
+
name = "edit <identity>";
|
|
1247
|
+
summary = "Open the file of one primitive in your editor";
|
|
1248
|
+
options = OPTIONS2;
|
|
1249
|
+
async run({ cwd, charterAuthoringApp: charterAuthoringApp2 }, { identity }) {
|
|
1250
|
+
const { scopedPrimitive: scopedPrimitive2 } = (await charterAuthoringApp2.open(identity ?? "")).data;
|
|
1251
|
+
if (scopedPrimitive2.data.scope === BUILTIN_SCOPE)
|
|
1252
|
+
throw new Fault(
|
|
1253
|
+
`${scopedPrimitive2.data.identity} is built into cw, and there is no file of it in this repository to edit.`,
|
|
1254
|
+
`To differ from it, run "cw add" for a primitive of your own under an identity of its own.`
|
|
1255
|
+
);
|
|
1256
|
+
const problem = await editInEditor(cwd, scopedPrimitive2.data.file);
|
|
1257
|
+
if (problem !== void 0) return { code: EXIT_FAILURE, problem };
|
|
1258
|
+
return {
|
|
1259
|
+
code: EXIT_OK,
|
|
1260
|
+
result: [`Closed ${scopedPrimitive2.data.file}.`, 'Run "cw doctor" to see that the charter still holds, then "cw build".', ""].join("\n")
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
};
|
|
1264
|
+
|
|
1265
|
+
// src/driver/cli/commands/ExplainCommand.ts
|
|
1266
|
+
var OPTIONS3 = {
|
|
1267
|
+
identity: {
|
|
1268
|
+
type: "string",
|
|
1269
|
+
describe: 'What the primitive is named by across the whole charter: "guide:no-any"'
|
|
1270
|
+
}
|
|
1271
|
+
};
|
|
1272
|
+
var ExplainCommand = class {
|
|
1273
|
+
name = "explain <identity>";
|
|
1274
|
+
summary = "Say which file declares an identity, and which layer it came from";
|
|
1275
|
+
options = OPTIONS3;
|
|
1276
|
+
async run({ charterAuthoringApp: charterAuthoringApp2 }, { identity }) {
|
|
1277
|
+
const explanationOutcomeDTO2 = await charterAuthoringApp2.explain(identity ?? "");
|
|
1278
|
+
if (explanationOutcomeDTO2.type === "FaultsByFile") {
|
|
1279
|
+
const noOfFiles = Object.keys(explanationOutcomeDTO2.data.files).length;
|
|
1280
|
+
return {
|
|
1281
|
+
code: EXIT_FAILURE,
|
|
1282
|
+
problem: `Nothing was explained: ${noOfFiles} file${noOfFiles === 1 ? " has" : "s have"} errors.
|
|
1283
|
+
Run "cw doctor" to see what is wrong with them.
|
|
1284
|
+
`
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
const { scopedPrimitive: scopedPrimitive2, activatesWhen, useMixins, rationale, hosts, citers, testCasesByFile: testCasesByFile2 } = explanationOutcomeDTO2.data;
|
|
1288
|
+
const citedRationale = scopedPrimitive2.data.headers.rationale;
|
|
1289
|
+
const declaredHeaders = Object.entries(scopedPrimitive2.data.headers).filter(([name]) => name !== "id" && name !== "description");
|
|
1290
|
+
return {
|
|
1291
|
+
code: EXIT_OK,
|
|
1292
|
+
result: [
|
|
1293
|
+
`${scopedPrimitive2.data.identity} ${scopedPrimitive2.data.description}`,
|
|
1294
|
+
` ${scopedPrimitive2.data.file}`,
|
|
1295
|
+
` ${layer(scopedPrimitive2.data.scope)}`,
|
|
1296
|
+
` comes up when ${activatesWhen}`,
|
|
1297
|
+
...declaredHeaders.map(([name, value]) => ` declares ${name}: ${typeof value === "string" ? value : value.join(", ")}`),
|
|
1298
|
+
...useMixins.map(({ data }) => ` uses mixin ${data.identity}`),
|
|
1299
|
+
...rationale !== void 0 ? [` rationale ${rationale.data.identity}`] : citedRationale !== void 0 ? [` rationale ${String(citedRationale)} (does not resolve)`] : [],
|
|
1300
|
+
...hosts.map(({ data }) => ` mixin of ${data.identity}`),
|
|
1301
|
+
...citers.map(({ data }) => ` rationale of ${data.identity}`),
|
|
1302
|
+
...Object.entries(testCasesByFile2.data).flatMap(
|
|
1303
|
+
([file, situations]) => situations.map((situation) => ` pinned down by ${file}: ${situation}`)
|
|
1304
|
+
),
|
|
1305
|
+
""
|
|
1306
|
+
].join("\n")
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
};
|
|
1310
|
+
function layer(scope) {
|
|
1311
|
+
const saidByScope = {
|
|
1312
|
+
[REPO_SCOPE]: "authored in this repository",
|
|
1313
|
+
[VENDOR_SCOPE]: "installed from a vendor",
|
|
1314
|
+
[BUILTIN_SCOPE]: "built into cw, and not authored in this repository"
|
|
1315
|
+
};
|
|
1316
|
+
return saidByScope[scope];
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
// src/driver/cli/commands/InitCommand.ts
|
|
1320
|
+
import prompts2 from "prompts";
|
|
1321
|
+
var OPTIONS4 = {
|
|
1322
|
+
agent: {
|
|
1323
|
+
type: "string",
|
|
1324
|
+
describe: `Which agent this charter compiles for: ${AGENT_PROVIDERS.join(", ")}`
|
|
1325
|
+
}
|
|
1326
|
+
};
|
|
1327
|
+
var InitCommand = class {
|
|
1328
|
+
name = "init";
|
|
1329
|
+
summary = "Set this repository up under a charter";
|
|
1330
|
+
options = OPTIONS4;
|
|
1331
|
+
async run({ charterAuthoringApp: charterAuthoringApp2 }, { agent }) {
|
|
1332
|
+
const agents = await whichAgents(charterAuthoringApp2, agent);
|
|
1333
|
+
await charterAuthoringApp2.init({ agents });
|
|
1334
|
+
return {
|
|
1335
|
+
code: EXIT_OK,
|
|
1336
|
+
result: [
|
|
1337
|
+
"Set up this repository under a charter.",
|
|
1338
|
+
`Compiling for: ${agents.join(", ")}.`,
|
|
1339
|
+
'Author your first rule under .cw/charter/guide/, then run "cw build".',
|
|
1340
|
+
""
|
|
1341
|
+
].join("\n")
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
};
|
|
1345
|
+
async function whichAgents(charterAuthoringApp2, typed) {
|
|
1346
|
+
if (typed !== void 0) {
|
|
1347
|
+
if (!isAgentProvider(typed))
|
|
1348
|
+
throw new Fault(
|
|
1349
|
+
`This engine compiles for no agent called "${typed}".`,
|
|
1350
|
+
`Name any of: ${AGENT_PROVIDERS.join(", ")}.`
|
|
1351
|
+
);
|
|
1352
|
+
return [typed];
|
|
1353
|
+
}
|
|
1354
|
+
const agentsInSettings = (await charterAuthoringApp2.settings()).data.agents.filter(isAgentProvider);
|
|
1355
|
+
if (!process.stdin.isTTY) {
|
|
1356
|
+
if (agentsInSettings.length === 0)
|
|
1357
|
+
throw new Fault(
|
|
1358
|
+
"Nobody is at the terminal to choose an agent, and this repository has none chosen yet.",
|
|
1359
|
+
`Run "cw init --agent <name>", naming one of: ${AGENT_PROVIDERS.join(", ")}.`
|
|
1360
|
+
);
|
|
1361
|
+
return agentsInSettings;
|
|
1362
|
+
}
|
|
1363
|
+
const { chosen } = await prompts2({
|
|
1364
|
+
type: "multiselect",
|
|
1365
|
+
name: "chosen",
|
|
1366
|
+
message: "Which agents does this charter compile for?",
|
|
1367
|
+
// What the settings already name arrives ticked, so a return keeps what this
|
|
1368
|
+
// repository is already running under and a second run is rechoosing rather
|
|
1369
|
+
// than starting over (FR-038). Nothing named yet — a repository being set up
|
|
1370
|
+
// for the first time — arrives with nothing ticked.
|
|
1371
|
+
choices: AGENT_PROVIDERS.map((one) => ({ title: one, value: one, selected: agentsInSettings.includes(one) })),
|
|
1372
|
+
// Ticking none of them is not an answer: a charter compiles for at least one
|
|
1373
|
+
// agent, so the prompt holds the submit until one is ticked (FR-033).
|
|
1374
|
+
min: 1
|
|
1375
|
+
});
|
|
1376
|
+
if (chosen === void 0)
|
|
1377
|
+
throw new Fault("Setup was stopped before it had its answers.", 'Run "cw init" again.');
|
|
1378
|
+
return chosen;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
// src/driver/cli/commands/KindsCommand.ts
|
|
1382
|
+
var OPTIONS5 = {
|
|
1383
|
+
kind: {
|
|
1384
|
+
type: "string",
|
|
1385
|
+
describe: "Which kind to be told what it requires; left off, every kind and when each comes up"
|
|
1386
|
+
}
|
|
1387
|
+
};
|
|
1388
|
+
var KindsCommand = class {
|
|
1389
|
+
name = "kinds [kind]";
|
|
1390
|
+
summary = "Say which kinds a charter is written in, and what one of them requires";
|
|
1391
|
+
options = OPTIONS5;
|
|
1392
|
+
async run({ charterAuthoringApp: charterAuthoringApp2 }, { kind }) {
|
|
1393
|
+
if (kind === void 0) {
|
|
1394
|
+
const { data: data2 } = await charterAuthoringApp2.kinds();
|
|
1395
|
+
return {
|
|
1396
|
+
code: EXIT_OK,
|
|
1397
|
+
result: `${Object.entries(data2).map(([named, activatesWhen]) => `${named} ${activatesWhen}`).join("\n")}
|
|
1398
|
+
`
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
const { data } = await charterAuthoringApp2.listPrimitiveRequirements(kind);
|
|
1402
|
+
const required = data.headers.filter((one) => one.data.required);
|
|
1403
|
+
const widestField = Math.max(...required.map((one) => one.data.field.length));
|
|
1404
|
+
return {
|
|
1405
|
+
code: EXIT_OK,
|
|
1406
|
+
result: [
|
|
1407
|
+
`A ${kind} requires:`,
|
|
1408
|
+
// A header read from a closed set is one of those values or refused, so
|
|
1409
|
+
// the values are part of what it requires (FR-118).
|
|
1410
|
+
...required.map(
|
|
1411
|
+
({ data: { field, shape, allowedValues } }) => ` ${field.padEnd(widestField)} ${shape}${allowedValues === void 0 ? "" : `, one of ${allowedValues.join(", ")}`}`
|
|
1412
|
+
),
|
|
1413
|
+
"",
|
|
1414
|
+
`Write one the way this ${kind} is written:`,
|
|
1415
|
+
data.sample,
|
|
1416
|
+
""
|
|
1417
|
+
].join("\n")
|
|
1418
|
+
};
|
|
1419
|
+
}
|
|
1420
|
+
};
|
|
1421
|
+
|
|
1422
|
+
// src/driver/cli/commands/ListCommand.ts
|
|
1423
|
+
var LISTING = {
|
|
1424
|
+
kind: { type: "string", describe: "Only the primitives of this kind" },
|
|
1425
|
+
min: {
|
|
1426
|
+
type: "boolean",
|
|
1427
|
+
describe: "What an agent surveys by: identity and description, nothing else",
|
|
1428
|
+
default: false
|
|
1429
|
+
}
|
|
1430
|
+
};
|
|
1431
|
+
var ListCommand = class {
|
|
1432
|
+
name = "list";
|
|
1433
|
+
summary = "Say what the charter holds";
|
|
1434
|
+
options = LISTING;
|
|
1435
|
+
async run({ charterAuthoringApp: charterAuthoringApp2 }, { kind, min }) {
|
|
1436
|
+
const catalogueDTO2 = await charterAuthoringApp2.list(kind);
|
|
1437
|
+
if (catalogueDTO2.type === "FaultsByFile") {
|
|
1438
|
+
const noOfFiles = Object.keys(catalogueDTO2.data.files).length;
|
|
1439
|
+
return {
|
|
1440
|
+
code: EXIT_FAILURE,
|
|
1441
|
+
problem: `Nothing was listed: ${noOfFiles} file${noOfFiles === 1 ? " has" : "s have"} errors.
|
|
1442
|
+
Run "cw doctor" to see what is wrong with them.
|
|
1443
|
+
`
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
const { entries } = catalogueDTO2.data;
|
|
1447
|
+
if (entries.length === 0)
|
|
1448
|
+
return {
|
|
1449
|
+
code: EXIT_OK,
|
|
1450
|
+
result: kind === void 0 ? "This charter holds nothing yet.\n" : `This charter holds no ${kind}.
|
|
1451
|
+
`
|
|
1452
|
+
};
|
|
1453
|
+
return {
|
|
1454
|
+
code: EXIT_OK,
|
|
1455
|
+
result: min ? `${entries.map(surveyed).join("\n")}
|
|
1456
|
+
` : `${entries.map(inFull).join("\n")}
|
|
1457
|
+
`
|
|
1458
|
+
};
|
|
1459
|
+
}
|
|
1460
|
+
};
|
|
1461
|
+
function surveyed({ data: { identity, description } }) {
|
|
1462
|
+
return `${identity} ${description}`;
|
|
1463
|
+
}
|
|
1464
|
+
function inFull(entry) {
|
|
1465
|
+
const { file, tags, globs, rationale, mixins } = entry.data;
|
|
1466
|
+
const declared = [
|
|
1467
|
+
...globs === void 0 ? [] : [`globs: ${globs.join(", ")}`],
|
|
1468
|
+
...tags === void 0 ? [] : [`tags: ${tags.join(", ")}`],
|
|
1469
|
+
...mixins === void 0 ? [] : [`mixins: ${mixins.join(", ")}`],
|
|
1470
|
+
...rationale === void 0 ? [] : [`rationale: ${rationale}`]
|
|
1471
|
+
];
|
|
1472
|
+
return [surveyed(entry), ` ${file}`, ...declared.length === 0 ? [] : [` ${declared.join(", ")}`]].join("\n");
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
// src/driver/portal/server.ts
|
|
1476
|
+
import { randomBytes } from "crypto";
|
|
1477
|
+
import { access } from "fs/promises";
|
|
1478
|
+
import { fileURLToPath } from "url";
|
|
1479
|
+
import { Hono } from "hono";
|
|
1480
|
+
import { bearerAuth } from "hono/bearer-auth";
|
|
1481
|
+
import { serve } from "@hono/node-server";
|
|
1482
|
+
import { serveStatic } from "@hono/node-server/serve-static";
|
|
1483
|
+
|
|
1484
|
+
// src/driver/portal/routes.ts
|
|
1485
|
+
import { createRoute, OpenAPIHono } from "@hono/zod-openapi";
|
|
1486
|
+
import { HTTPException } from "hono/http-exception";
|
|
1487
|
+
import { z as z10 } from "zod";
|
|
1488
|
+
|
|
1489
|
+
// src/hexagon/port/driver/dtos/dto.ts
|
|
1490
|
+
import { z as z5 } from "zod";
|
|
1491
|
+
var strings = z5.array(z5.string()).readonly();
|
|
1492
|
+
var dto = (type, data) => z5.object({ type: z5.literal(type), data });
|
|
1493
|
+
function byType(...schemas) {
|
|
1494
|
+
return Object.fromEntries(schemas.map((one) => [one.shape.type.value, one]));
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
// src/hexagon/port/driver/dtos/data.ts
|
|
1498
|
+
import { z as z6 } from "zod";
|
|
1499
|
+
var fault = dto(
|
|
1500
|
+
"Fault",
|
|
1501
|
+
z6.object({
|
|
1502
|
+
message: z6.string(),
|
|
1503
|
+
fix: z6.string(),
|
|
1504
|
+
severity: z6.enum(["error", "warn"])
|
|
1505
|
+
})
|
|
1506
|
+
);
|
|
1507
|
+
var faultsByFile = dto(
|
|
1508
|
+
"FaultsByFile",
|
|
1509
|
+
z6.object({
|
|
1510
|
+
files: z6.record(z6.string(), z6.array(fault).readonly())
|
|
1511
|
+
})
|
|
1512
|
+
);
|
|
1513
|
+
var faults = dto(
|
|
1514
|
+
"Faults",
|
|
1515
|
+
z6.object({
|
|
1516
|
+
faults: z6.array(fault).readonly()
|
|
1517
|
+
})
|
|
1518
|
+
);
|
|
1519
|
+
var primitiveKinds = dto("PrimitiveKinds", z6.record(z6.string(), z6.string()));
|
|
1520
|
+
var catalogueEntry = dto(
|
|
1521
|
+
"CatalogueEntry",
|
|
1522
|
+
z6.object({
|
|
1523
|
+
identity: z6.string(),
|
|
1524
|
+
kind: z6.string(),
|
|
1525
|
+
id: z6.string(),
|
|
1526
|
+
description: z6.string(),
|
|
1527
|
+
file: z6.string(),
|
|
1528
|
+
tags: strings.optional(),
|
|
1529
|
+
globs: strings.optional(),
|
|
1530
|
+
rationale: z6.string().optional(),
|
|
1531
|
+
mixins: strings.optional()
|
|
1532
|
+
})
|
|
1533
|
+
);
|
|
1534
|
+
var catalogue = dto(
|
|
1535
|
+
"Catalogue",
|
|
1536
|
+
z6.object({
|
|
1537
|
+
entries: z6.array(catalogueEntry).readonly()
|
|
1538
|
+
})
|
|
1539
|
+
);
|
|
1540
|
+
var scopedPrimitive = dto(
|
|
1541
|
+
"ScopedPrimitive",
|
|
1542
|
+
z6.object({
|
|
1543
|
+
identity: z6.string(),
|
|
1544
|
+
kind: z6.string(),
|
|
1545
|
+
description: z6.string(),
|
|
1546
|
+
file: z6.string(),
|
|
1547
|
+
scope: z6.enum(["repo", "vendor", "builtin"]),
|
|
1548
|
+
headers: z6.record(z6.string(), z6.union([z6.string(), strings]))
|
|
1549
|
+
})
|
|
1550
|
+
);
|
|
1551
|
+
var scopedPrimitives = dto(
|
|
1552
|
+
"ScopedPrimitives",
|
|
1553
|
+
z6.object({
|
|
1554
|
+
primitives: z6.array(scopedPrimitive).readonly()
|
|
1555
|
+
})
|
|
1556
|
+
);
|
|
1557
|
+
var primitiveSnapshot = dto(
|
|
1558
|
+
"PrimitiveSnapshot",
|
|
1559
|
+
z6.object({
|
|
1560
|
+
scopedPrimitive,
|
|
1561
|
+
body: z6.string(),
|
|
1562
|
+
revision: z6.string()
|
|
1563
|
+
})
|
|
1564
|
+
);
|
|
1565
|
+
var planSummary = dto(
|
|
1566
|
+
"PlanSummary",
|
|
1567
|
+
z6.object({
|
|
1568
|
+
added: strings,
|
|
1569
|
+
edited: strings,
|
|
1570
|
+
deleted: strings,
|
|
1571
|
+
unchanged: strings
|
|
1572
|
+
})
|
|
1573
|
+
);
|
|
1574
|
+
var primitiveHeader = dto(
|
|
1575
|
+
"PrimitiveHeader",
|
|
1576
|
+
z6.object({
|
|
1577
|
+
field: z6.string(),
|
|
1578
|
+
shape: z6.enum(["line", "list"]),
|
|
1579
|
+
required: z6.boolean(),
|
|
1580
|
+
allowedValues: strings.optional()
|
|
1581
|
+
})
|
|
1582
|
+
);
|
|
1583
|
+
var primitiveRequirements = dto(
|
|
1584
|
+
"PrimitiveRequirements",
|
|
1585
|
+
z6.object({
|
|
1586
|
+
headers: z6.array(primitiveHeader).readonly(),
|
|
1587
|
+
sample: z6.string()
|
|
1588
|
+
})
|
|
1589
|
+
);
|
|
1590
|
+
var testCasesByFile = dto("TestCasesByFile", z6.record(z6.string(), strings));
|
|
1591
|
+
var testCaseReport = dto(
|
|
1592
|
+
"TestCaseReport",
|
|
1593
|
+
z6.object({
|
|
1594
|
+
suiteName: z6.string(),
|
|
1595
|
+
situation: z6.string(),
|
|
1596
|
+
passed: z6.boolean(),
|
|
1597
|
+
unmet: fault.optional()
|
|
1598
|
+
})
|
|
1599
|
+
);
|
|
1600
|
+
var testRunReport = dto(
|
|
1601
|
+
"TestRunReport",
|
|
1602
|
+
z6.object({
|
|
1603
|
+
testCaseReports: z6.array(testCaseReport).readonly()
|
|
1604
|
+
})
|
|
1605
|
+
);
|
|
1606
|
+
var testCase = dto(
|
|
1607
|
+
"TestCase",
|
|
1608
|
+
z6.object({
|
|
1609
|
+
situation: z6.string(),
|
|
1610
|
+
expectation: z6.string(),
|
|
1611
|
+
identity: z6.string().optional()
|
|
1612
|
+
})
|
|
1613
|
+
);
|
|
1614
|
+
var testSuite = dto(
|
|
1615
|
+
"TestSuite",
|
|
1616
|
+
z6.object({
|
|
1617
|
+
name: z6.string(),
|
|
1618
|
+
text: z6.string().optional(),
|
|
1619
|
+
description: z6.string().optional(),
|
|
1620
|
+
cases: z6.array(testCase).readonly(),
|
|
1621
|
+
fault: fault.optional()
|
|
1622
|
+
})
|
|
1623
|
+
);
|
|
1624
|
+
var testSuites = dto(
|
|
1625
|
+
"TestSuites",
|
|
1626
|
+
z6.object({
|
|
1627
|
+
testSuites: z6.array(testSuite).readonly()
|
|
1628
|
+
})
|
|
1629
|
+
);
|
|
1630
|
+
var workspaceSettings = dto(
|
|
1631
|
+
"WorkspaceSettings",
|
|
1632
|
+
z6.object({
|
|
1633
|
+
agents: strings
|
|
1634
|
+
})
|
|
1635
|
+
);
|
|
1636
|
+
var DataDTOs = byType(
|
|
1637
|
+
catalogue,
|
|
1638
|
+
catalogueEntry,
|
|
1639
|
+
fault,
|
|
1640
|
+
faults,
|
|
1641
|
+
faultsByFile,
|
|
1642
|
+
planSummary,
|
|
1643
|
+
primitiveHeader,
|
|
1644
|
+
primitiveKinds,
|
|
1645
|
+
primitiveRequirements,
|
|
1646
|
+
primitiveSnapshot,
|
|
1647
|
+
scopedPrimitive,
|
|
1648
|
+
scopedPrimitives,
|
|
1649
|
+
testCase,
|
|
1650
|
+
testCaseReport,
|
|
1651
|
+
testCasesByFile,
|
|
1652
|
+
testRunReport,
|
|
1653
|
+
testSuite,
|
|
1654
|
+
testSuites,
|
|
1655
|
+
workspaceSettings
|
|
1656
|
+
);
|
|
1657
|
+
|
|
1658
|
+
// src/hexagon/port/driver/dtos/outcome.ts
|
|
1659
|
+
import { z as z7 } from "zod";
|
|
1660
|
+
var doctorOutcome = dto(
|
|
1661
|
+
"DoctorOutcome",
|
|
1662
|
+
z7.object({
|
|
1663
|
+
agents: strings,
|
|
1664
|
+
errorCount: z7.number(),
|
|
1665
|
+
warnCount: z7.number(),
|
|
1666
|
+
pendingCount: z7.number().nullable(),
|
|
1667
|
+
faultsByFile: DataDTOs.FaultsByFile,
|
|
1668
|
+
driftedVendors: strings,
|
|
1669
|
+
problemCount: z7.number()
|
|
1670
|
+
})
|
|
1671
|
+
);
|
|
1672
|
+
var explanationOutcome = dto(
|
|
1673
|
+
"ExplanationOutcome",
|
|
1674
|
+
z7.object({
|
|
1675
|
+
scopedPrimitive: DataDTOs.ScopedPrimitive,
|
|
1676
|
+
activatesWhen: z7.string(),
|
|
1677
|
+
useMixins: z7.array(DataDTOs.ScopedPrimitive).readonly(),
|
|
1678
|
+
rationale: DataDTOs.ScopedPrimitive.optional(),
|
|
1679
|
+
hosts: z7.array(DataDTOs.ScopedPrimitive).readonly(),
|
|
1680
|
+
citers: z7.array(DataDTOs.ScopedPrimitive).readonly(),
|
|
1681
|
+
testCasesByFile: DataDTOs.TestCasesByFile
|
|
1682
|
+
})
|
|
1683
|
+
);
|
|
1684
|
+
var OutcomeDTOs = byType(doctorOutcome, explanationOutcome);
|
|
1685
|
+
|
|
1686
|
+
// src/hexagon/domain/models/test/TestSuite.ts
|
|
1687
|
+
import { z as z9 } from "zod";
|
|
1688
|
+
|
|
1689
|
+
// src/hexagon/domain/models/test/TestCase.ts
|
|
1690
|
+
import { z as z8 } from "zod";
|
|
1691
|
+
var TEST_CASE_SCHEMA = z8.union([
|
|
1692
|
+
z8.strictObject({
|
|
1693
|
+
do: z8.strictObject({ touchFile: z8.string().min(1) }),
|
|
1694
|
+
expect: z8.union([
|
|
1695
|
+
z8.strictObject({ activate: PRIMITIVE_IDENTITY_SCHEMA }),
|
|
1696
|
+
z8.strictObject({ allow: z8.boolean() })
|
|
1697
|
+
])
|
|
1698
|
+
}),
|
|
1699
|
+
z8.strictObject({
|
|
1700
|
+
when: z8.enum(SIGNALS),
|
|
1701
|
+
expect: z8.strictObject({ run: PRIMITIVE_IDENTITY_SCHEMA })
|
|
1702
|
+
})
|
|
1703
|
+
]);
|
|
1704
|
+
var TestCase = class {
|
|
1705
|
+
constructor(written) {
|
|
1706
|
+
this.written = written;
|
|
1707
|
+
}
|
|
1708
|
+
written;
|
|
1709
|
+
/** The situation it puts, said as a line, and what it expects of it: what a
|
|
1710
|
+
* run reports it under, and what an explanation names it by (FR-048,
|
|
1711
|
+
* FR-014). */
|
|
1712
|
+
describe() {
|
|
1713
|
+
return `${this.situation} ${this.expectation}`;
|
|
1714
|
+
}
|
|
1715
|
+
/** The situation it puts: the file it touches, or the event it raises. */
|
|
1716
|
+
get situation() {
|
|
1717
|
+
return "when" in this.written ? `firing event ${this.written.when}` : `touching ${this.written.do.touchFile}`;
|
|
1718
|
+
}
|
|
1719
|
+
/** What it expects the charter to make of its situation. */
|
|
1720
|
+
get expectation() {
|
|
1721
|
+
const written = this.written;
|
|
1722
|
+
if ("when" in written) return `runs ${written.expect.run}`;
|
|
1723
|
+
if ("activate" in written.expect) return `activates ${written.expect.activate}`;
|
|
1724
|
+
return `is ${written.expect.allow ? "allowed" : "denied"}`;
|
|
1725
|
+
}
|
|
1726
|
+
/**
|
|
1727
|
+
* Which primitive this case expects to be activated: the sensor a raised event
|
|
1728
|
+
* is expected to run, or the guide a touched file is expected to bring up
|
|
1729
|
+
* (FR-014).
|
|
1730
|
+
*
|
|
1731
|
+
* Nothing for the third shape. That one expects a touched file to be allowed
|
|
1732
|
+
* or denied, which every posture in the charter answers between them, so it
|
|
1733
|
+
* activates no one primitive by name.
|
|
1734
|
+
*/
|
|
1735
|
+
get activatedIdentity() {
|
|
1736
|
+
if ("when" in this.written) return this.written.expect.run;
|
|
1737
|
+
if ("activate" in this.written.expect) return this.written.expect.activate;
|
|
1738
|
+
return void 0;
|
|
1739
|
+
}
|
|
1740
|
+
};
|
|
1741
|
+
|
|
1742
|
+
// src/hexagon/domain/models/test/TestSuite.ts
|
|
1743
|
+
var TEST_SUITE_SCHEMA = z9.strictObject({
|
|
1744
|
+
description: z9.string().min(1).optional(),
|
|
1745
|
+
cases: z9.array(TEST_CASE_SCHEMA.transform((written) => new TestCase(written))).min(1)
|
|
1746
|
+
});
|
|
1747
|
+
var TestSuite = class {
|
|
1748
|
+
constructor(description, cases, body) {
|
|
1749
|
+
this.description = description;
|
|
1750
|
+
this.cases = cases;
|
|
1751
|
+
this.body = body;
|
|
1752
|
+
}
|
|
1753
|
+
description;
|
|
1754
|
+
cases;
|
|
1755
|
+
body;
|
|
1756
|
+
};
|
|
1757
|
+
var SAMPLE_CASES = [
|
|
1758
|
+
{ do: { touchFile: "src/one.ts" }, expect: { activate: "guide:no-any" } },
|
|
1759
|
+
{ do: { touchFile: ".env" }, expect: { allow: false } },
|
|
1760
|
+
{ when: "PreToolUse", expect: { run: "sensor:no-secrets" } }
|
|
1761
|
+
];
|
|
1762
|
+
var SAMPLE = JSON.stringify({ cases: SAMPLE_CASES }, void 0, 1).replace(/\n\s*/g, " ");
|
|
1763
|
+
var DEFAULT_TEST_SUITE_BODY = `${JSON.stringify({ description: "What these cases pin down.", cases: [SAMPLE_CASES[0]] }, null, 2)}
|
|
1764
|
+
`;
|
|
1765
|
+
function testSuiteOf(text) {
|
|
1766
|
+
let written;
|
|
1767
|
+
try {
|
|
1768
|
+
written = JSON.parse(text);
|
|
1769
|
+
} catch {
|
|
1770
|
+
throw new TestSuiteFault(
|
|
1771
|
+
`This file is not JSON, so nothing can read the cases it was meant to hold.`,
|
|
1772
|
+
`Write it as ${SAMPLE}`
|
|
1773
|
+
);
|
|
1774
|
+
}
|
|
1775
|
+
const read = TEST_SUITE_SCHEMA.safeParse(written);
|
|
1776
|
+
if (!read.success) throw new TestSuiteFault(`This file is not written as a suite of cases.`, `Write it as ${SAMPLE}`);
|
|
1777
|
+
return new TestSuite(read.data.description, read.data.cases, text);
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
// src/hexagon/domain/models/test/TestRoot.ts
|
|
1781
|
+
function testSuiteNameOf(path) {
|
|
1782
|
+
return path.slice(`${TEST_DIRECTORY}/`.length);
|
|
1783
|
+
}
|
|
1784
|
+
var TestRoot = class {
|
|
1785
|
+
constructor(suitesByFile, faultsByFiles) {
|
|
1786
|
+
this.suitesByFile = suitesByFile;
|
|
1787
|
+
this.faultsByFiles = faultsByFiles;
|
|
1788
|
+
}
|
|
1789
|
+
suitesByFile;
|
|
1790
|
+
faultsByFiles;
|
|
1791
|
+
/** Whether a test file is under this name, whether it reads or not. A name
|
|
1792
|
+
* reaching outside `.cw/test/` is no name of one. */
|
|
1793
|
+
hasTestSuiteNamed(name) {
|
|
1794
|
+
const path = `${TEST_DIRECTORY}/${name}`;
|
|
1795
|
+
return path in this.suitesByFile || path in this.faultsByFiles.files;
|
|
1796
|
+
}
|
|
1797
|
+
/** What a new test file is named: `untitled-<n>.json` for the first `n` no
|
|
1798
|
+
* test file has (FR-091). */
|
|
1799
|
+
get untitledName() {
|
|
1800
|
+
let untitledNumber = 1;
|
|
1801
|
+
while (this.hasTestSuiteNamed(`untitled-${untitledNumber}.json`)) untitledNumber++;
|
|
1802
|
+
return `untitled-${untitledNumber}.json`;
|
|
1803
|
+
}
|
|
1804
|
+
};
|
|
1805
|
+
function testRootOf(testSuiteFiles) {
|
|
1806
|
+
const suitesByFile = {};
|
|
1807
|
+
const faultsByFiles = {};
|
|
1808
|
+
for (const { path, contents } of [...testSuiteFiles].sort((one, another) => one.path.localeCompare(another.path))) {
|
|
1809
|
+
try {
|
|
1810
|
+
suitesByFile[path] = testSuiteOf(contents);
|
|
1811
|
+
} catch (raised) {
|
|
1812
|
+
if (!(raised instanceof Fault)) throw raised;
|
|
1813
|
+
faultsByFiles[path] = [raised];
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
return new TestRoot(suitesByFile, new FaultsByFile(faultsByFiles));
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
// src/hexagon/application/dtos.ts
|
|
1820
|
+
function catalogueDTO(catalogue2) {
|
|
1821
|
+
return { type: "Catalogue", data: { entries: catalogue2.full.map((one) => ({ type: "CatalogueEntry", data: one })) } };
|
|
1822
|
+
}
|
|
1823
|
+
function doctorOutcomeDTO({ agents }, faultsByFile2, planSummary2, driftedVendors2, repo) {
|
|
1824
|
+
const faults2 = Object.values(faultsByFile2.files).flat();
|
|
1825
|
+
const errorCount = faults2.filter((one) => one.severity === "error").length;
|
|
1826
|
+
const pendingCount = planSummary2 === void 0 ? null : planSummary2.added.length + planSummary2.edited.length + planSummary2.deleted.length;
|
|
1827
|
+
const unwell = [errorCount > 0, driftedVendors2.length > 0, pendingCount !== 0];
|
|
1828
|
+
return {
|
|
1829
|
+
type: "DoctorOutcome",
|
|
1830
|
+
data: {
|
|
1831
|
+
agents,
|
|
1832
|
+
errorCount,
|
|
1833
|
+
warnCount: faults2.filter((one) => one.severity === "warn").length,
|
|
1834
|
+
pendingCount,
|
|
1835
|
+
faultsByFile: faultsByFileDTO(faultsByFile2, repo),
|
|
1836
|
+
driftedVendors: driftedVendors2,
|
|
1837
|
+
problemCount: unwell.filter(Boolean).length
|
|
1838
|
+
}
|
|
1839
|
+
};
|
|
1840
|
+
}
|
|
1841
|
+
function explanationOutcomeDTO(scopedPrimitive2, activatesWhen, useMixins, rationale, hosts, citers, testCasesByFile2) {
|
|
1842
|
+
return {
|
|
1843
|
+
type: "ExplanationOutcome",
|
|
1844
|
+
data: {
|
|
1845
|
+
scopedPrimitive: scopedPrimitiveDTO(scopedPrimitive2),
|
|
1846
|
+
activatesWhen,
|
|
1847
|
+
useMixins: useMixins.map(scopedPrimitiveDTO),
|
|
1848
|
+
...rationale === void 0 ? {} : { rationale: scopedPrimitiveDTO(rationale) },
|
|
1849
|
+
hosts: hosts.map(scopedPrimitiveDTO),
|
|
1850
|
+
citers: citers.map(scopedPrimitiveDTO),
|
|
1851
|
+
testCasesByFile: { type: "TestCasesByFile", data: testCasesByFile2 }
|
|
1852
|
+
}
|
|
1853
|
+
};
|
|
1854
|
+
}
|
|
1855
|
+
function faultDTO({ message, fix, severity }) {
|
|
1856
|
+
return { type: "Fault", data: { message, fix, severity } };
|
|
1857
|
+
}
|
|
1858
|
+
function faultsDTO(faults2) {
|
|
1859
|
+
return { type: "Faults", data: { faults: faults2.faults.map(faultDTO) } };
|
|
1860
|
+
}
|
|
1861
|
+
function faultsByFileDTO(faultsByFile2, repo) {
|
|
1862
|
+
return {
|
|
1863
|
+
type: "FaultsByFile",
|
|
1864
|
+
data: {
|
|
1865
|
+
files: Object.fromEntries(
|
|
1866
|
+
Object.entries(faultsByFile2.namedFrom(repo).files).map(([file, faults2]) => [file, faults2.map(faultDTO)])
|
|
1867
|
+
)
|
|
1868
|
+
}
|
|
1869
|
+
};
|
|
1870
|
+
}
|
|
1871
|
+
function primitiveSnapshotDTO(scopedPrimitive2, revision) {
|
|
1872
|
+
return {
|
|
1873
|
+
type: "PrimitiveSnapshot",
|
|
1874
|
+
data: { scopedPrimitive: scopedPrimitiveDTO(scopedPrimitive2), body: scopedPrimitive2.primitive.body, revision }
|
|
1875
|
+
};
|
|
1876
|
+
}
|
|
1877
|
+
function planSummaryDTO({ added, edited, deleted, unchanged }) {
|
|
1878
|
+
return { type: "PlanSummary", data: { added, edited, deleted, unchanged } };
|
|
1879
|
+
}
|
|
1880
|
+
function primitiveHeaderDTO({ field, shape, required, allowedValues }) {
|
|
1881
|
+
return {
|
|
1882
|
+
type: "PrimitiveHeader",
|
|
1883
|
+
data: { field, shape, required, ...allowedValues === void 0 ? {} : { allowedValues } }
|
|
1884
|
+
};
|
|
1885
|
+
}
|
|
1886
|
+
function primitiveRequirementsDTO({ headers, sample }) {
|
|
1887
|
+
return { type: "PrimitiveRequirements", data: { headers: headers.map(primitiveHeaderDTO), sample } };
|
|
1888
|
+
}
|
|
1889
|
+
function primitiveKindsDTO(kinds) {
|
|
1890
|
+
return { type: "PrimitiveKinds", data: Object.fromEntries(kinds.map((one) => [one.kind, one.activatesWhen])) };
|
|
1891
|
+
}
|
|
1892
|
+
function scopedPrimitiveDTO({ identity, scope, file, primitive }) {
|
|
1893
|
+
return {
|
|
1894
|
+
type: "ScopedPrimitive",
|
|
1895
|
+
data: {
|
|
1896
|
+
identity,
|
|
1897
|
+
kind: primitive.kind,
|
|
1898
|
+
description: primitive.headers.description,
|
|
1899
|
+
file,
|
|
1900
|
+
scope,
|
|
1901
|
+
// A header nobody wrote is left out rather than carried as undefined: this
|
|
1902
|
+
// says what the file declared.
|
|
1903
|
+
headers: Object.fromEntries(
|
|
1904
|
+
Object.entries(primitive.headers).filter(
|
|
1905
|
+
(entry) => entry[1] !== void 0
|
|
1906
|
+
)
|
|
1907
|
+
)
|
|
1908
|
+
}
|
|
1909
|
+
};
|
|
1910
|
+
}
|
|
1911
|
+
function scopedPrimitivesDTO(scopedPrimitives2) {
|
|
1912
|
+
return {
|
|
1913
|
+
type: "ScopedPrimitives",
|
|
1914
|
+
data: {
|
|
1915
|
+
primitives: [...scopedPrimitives2].sort((one, another) => one.identity < another.identity ? -1 : one.identity > another.identity ? 1 : 0).map(scopedPrimitiveDTO)
|
|
1916
|
+
}
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1919
|
+
function testCaseReportDTO({ suiteName, situation, passed, unmet }) {
|
|
1920
|
+
return {
|
|
1921
|
+
type: "TestCaseReport",
|
|
1922
|
+
data: { suiteName, situation, passed, ...unmet === void 0 ? {} : { unmet: faultDTO(unmet) } }
|
|
1923
|
+
};
|
|
1924
|
+
}
|
|
1925
|
+
function testRunReportDTO({ testCaseReports }) {
|
|
1926
|
+
return { type: "TestRunReport", data: { testCaseReports: testCaseReports.map(testCaseReportDTO) } };
|
|
1927
|
+
}
|
|
1928
|
+
function testSuitesDTO({ suitesByFile, faultsByFiles }) {
|
|
1929
|
+
const paths = [...Object.keys(suitesByFile), ...Object.keys(faultsByFiles.files)].sort((one, another) => one.localeCompare(another));
|
|
1930
|
+
return {
|
|
1931
|
+
type: "TestSuites",
|
|
1932
|
+
data: {
|
|
1933
|
+
testSuites: paths.map((path) => {
|
|
1934
|
+
const suite = suitesByFile[path];
|
|
1935
|
+
const [fault2] = faultsByFiles.files[path] ?? [];
|
|
1936
|
+
return {
|
|
1937
|
+
type: "TestSuite",
|
|
1938
|
+
data: {
|
|
1939
|
+
name: testSuiteNameOf(path),
|
|
1940
|
+
...suite === void 0 ? {} : { text: suite.body },
|
|
1941
|
+
...suite?.description === void 0 ? {} : { description: suite.description },
|
|
1942
|
+
cases: (suite?.cases ?? []).map(({ situation, expectation, activatedIdentity }) => ({
|
|
1943
|
+
type: "TestCase",
|
|
1944
|
+
data: { situation, expectation, ...activatedIdentity === void 0 ? {} : { identity: activatedIdentity } }
|
|
1945
|
+
})),
|
|
1946
|
+
...fault2 === void 0 ? {} : { fault: faultDTO(fault2) }
|
|
1947
|
+
}
|
|
1948
|
+
};
|
|
1949
|
+
})
|
|
1950
|
+
}
|
|
1951
|
+
};
|
|
1952
|
+
}
|
|
1953
|
+
function workspaceSettingsDTO({ agents }) {
|
|
1954
|
+
return { type: "WorkspaceSettings", data: { agents } };
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
// src/driver/portal/routes.ts
|
|
1958
|
+
var json = (schema, description) => ({
|
|
1959
|
+
content: { "application/json": { schema } },
|
|
1960
|
+
description
|
|
1961
|
+
});
|
|
1962
|
+
function api(charterAuthoringApp2, charterVendoringApp2, testAuthoringApp2) {
|
|
1963
|
+
const app = new OpenAPIHono();
|
|
1964
|
+
app.onError((raised, c) => {
|
|
1965
|
+
if (raised instanceof Fault) return c.json(faultDTO(raised), 422);
|
|
1966
|
+
if (raised instanceof HTTPException) return raised.getResponse();
|
|
1967
|
+
console.error(raised);
|
|
1968
|
+
return c.text(raised.message, 500);
|
|
1969
|
+
});
|
|
1970
|
+
return app.openapi(
|
|
1971
|
+
createRoute({
|
|
1972
|
+
method: "get",
|
|
1973
|
+
path: "/charter/root/faults",
|
|
1974
|
+
responses: { 200: json(DataDTOs.FaultsByFile, "Everything wrong with the charter, under each file") }
|
|
1975
|
+
}),
|
|
1976
|
+
async (c) => c.json((await charterAuthoringApp2.doctor()).data.faultsByFile, 200)
|
|
1977
|
+
).openapi(
|
|
1978
|
+
createRoute({
|
|
1979
|
+
method: "get",
|
|
1980
|
+
path: "/charter/root/primitives",
|
|
1981
|
+
request: { query: z10.object({ matching: z10.string().optional() }) },
|
|
1982
|
+
responses: {
|
|
1983
|
+
200: json(DataDTOs.ScopedPrimitives, "Every primitive of every layer, or those mentioning the word"),
|
|
1984
|
+
422: json(z10.union([DataDTOs.FaultsByFile, DataDTOs.Fault]), "The charter does not hold")
|
|
1985
|
+
}
|
|
1986
|
+
}),
|
|
1987
|
+
async (c) => {
|
|
1988
|
+
const scopedPrimitivesDTO2 = await charterAuthoringApp2.fullList(c.req.valid("query").matching || void 0);
|
|
1989
|
+
return scopedPrimitivesDTO2.type === "FaultsByFile" ? c.json(scopedPrimitivesDTO2, 422) : c.json(scopedPrimitivesDTO2, 200);
|
|
1990
|
+
}
|
|
1991
|
+
).openapi(
|
|
1992
|
+
createRoute({
|
|
1993
|
+
method: "get",
|
|
1994
|
+
path: "/charter/root/primitives/{identity}/explanation",
|
|
1995
|
+
request: { params: z10.object({ identity: z10.string() }) },
|
|
1996
|
+
responses: {
|
|
1997
|
+
200: json(OutcomeDTOs.ExplanationOutcome, "What the engine says of one primitive"),
|
|
1998
|
+
422: json(z10.union([DataDTOs.FaultsByFile, DataDTOs.Fault]), "The charter does not hold, or holds nothing of it")
|
|
1999
|
+
}
|
|
2000
|
+
}),
|
|
2001
|
+
async (c) => {
|
|
2002
|
+
const { identity } = c.req.valid("param");
|
|
2003
|
+
const explanationOutcomeDTO2 = await charterAuthoringApp2.explain(identity);
|
|
2004
|
+
return explanationOutcomeDTO2.type === "FaultsByFile" ? c.json(explanationOutcomeDTO2, 422) : c.json(explanationOutcomeDTO2, 200);
|
|
2005
|
+
}
|
|
2006
|
+
).openapi(
|
|
2007
|
+
createRoute({
|
|
2008
|
+
method: "post",
|
|
2009
|
+
path: "/charter/root/primitives",
|
|
2010
|
+
request: {
|
|
2011
|
+
body: json(
|
|
2012
|
+
z10.object({
|
|
2013
|
+
kind: z10.string(),
|
|
2014
|
+
id: z10.string(),
|
|
2015
|
+
headers: z10.record(z10.string(), z10.union([z10.string(), z10.array(z10.string())])),
|
|
2016
|
+
body: z10.string()
|
|
2017
|
+
}),
|
|
2018
|
+
"A new primitive: its kind and id, a line or a list under each header, and its body"
|
|
2019
|
+
)
|
|
2020
|
+
},
|
|
2021
|
+
responses: {
|
|
2022
|
+
201: {
|
|
2023
|
+
...json(DataDTOs.ScopedPrimitive, "The primitive written"),
|
|
2024
|
+
headers: z10.object({ Location: z10.string() })
|
|
2025
|
+
},
|
|
2026
|
+
422: json(z10.union([DataDTOs.Faults, DataDTOs.Fault]), "The answers the kind refused, or an identity already claimed")
|
|
2027
|
+
}
|
|
2028
|
+
}),
|
|
2029
|
+
async (c) => {
|
|
2030
|
+
const { kind, id, headers, body } = c.req.valid("json");
|
|
2031
|
+
const scopedPrimitiveDTO2 = await charterAuthoringApp2.add(kind, id, headers, body);
|
|
2032
|
+
if (scopedPrimitiveDTO2.type === "Faults") return c.json(scopedPrimitiveDTO2, 422);
|
|
2033
|
+
return c.json(scopedPrimitiveDTO2, 201, { Location: `/api/charter/root/primitives/${scopedPrimitiveDTO2.data.identity}` });
|
|
2034
|
+
}
|
|
2035
|
+
).openapi(
|
|
2036
|
+
createRoute({
|
|
2037
|
+
method: "get",
|
|
2038
|
+
path: "/charter/root/primitives/{identity}",
|
|
2039
|
+
request: { params: z10.object({ identity: z10.string() }) },
|
|
2040
|
+
responses: {
|
|
2041
|
+
200: {
|
|
2042
|
+
...json(DataDTOs.PrimitiveSnapshot, "One primitive as the charter read it"),
|
|
2043
|
+
headers: z10.object({ ETag: z10.string() })
|
|
2044
|
+
},
|
|
2045
|
+
422: json(DataDTOs.Fault, "The charter holds nothing of it")
|
|
2046
|
+
}
|
|
2047
|
+
}),
|
|
2048
|
+
async (c) => {
|
|
2049
|
+
const { identity } = c.req.valid("param");
|
|
2050
|
+
const primitiveSnapshotDTO2 = await charterAuthoringApp2.open(identity);
|
|
2051
|
+
return c.json(primitiveSnapshotDTO2, 200, { ETag: `"${primitiveSnapshotDTO2.data.revision}"` });
|
|
2052
|
+
}
|
|
2053
|
+
).openapi(
|
|
2054
|
+
createRoute({
|
|
2055
|
+
method: "put",
|
|
2056
|
+
path: "/charter/root/primitives/{identity}",
|
|
2057
|
+
request: {
|
|
2058
|
+
params: z10.object({ identity: z10.string() }),
|
|
2059
|
+
headers: z10.object({ "if-match": z10.string().optional() }),
|
|
2060
|
+
body: json(
|
|
2061
|
+
z10.object({
|
|
2062
|
+
headers: z10.record(z10.string(), z10.union([z10.string(), z10.array(z10.string())])),
|
|
2063
|
+
body: z10.string()
|
|
2064
|
+
}),
|
|
2065
|
+
"A line or a list under each header, and the body, written over the primitive's own"
|
|
2066
|
+
)
|
|
2067
|
+
},
|
|
2068
|
+
responses: {
|
|
2069
|
+
200: json(DataDTOs.ScopedPrimitive, "The primitive written"),
|
|
2070
|
+
412: json(DataDTOs.Fault, "The file changed on disk after it was opened"),
|
|
2071
|
+
422: json(z10.union([DataDTOs.Faults, DataDTOs.Fault]), "The answers the kind refused, or a primitive this repository did not author")
|
|
2072
|
+
}
|
|
2073
|
+
}),
|
|
2074
|
+
async (c) => {
|
|
2075
|
+
const { identity } = c.req.valid("param");
|
|
2076
|
+
const { revision, scopedPrimitive: scopedPrimitive2 } = (await charterAuthoringApp2.open(identity)).data;
|
|
2077
|
+
if (c.req.valid("header")["if-match"] !== `"${revision}"`)
|
|
2078
|
+
return c.json(
|
|
2079
|
+
faultDTO(
|
|
2080
|
+
new Fault(
|
|
2081
|
+
`${scopedPrimitive2.data.file} changed on disk after it was opened, and saving would write over that change.`,
|
|
2082
|
+
"Open it again to see what changed, then make your edit there."
|
|
2083
|
+
)
|
|
2084
|
+
),
|
|
2085
|
+
412
|
|
2086
|
+
);
|
|
2087
|
+
const { headers, body } = c.req.valid("json");
|
|
2088
|
+
const scopedPrimitiveDTO2 = await charterAuthoringApp2.rewrite(identity, headers, body, revision);
|
|
2089
|
+
return scopedPrimitiveDTO2.type === "Faults" ? c.json(scopedPrimitiveDTO2, 422) : c.json(scopedPrimitiveDTO2, 200);
|
|
2090
|
+
}
|
|
2091
|
+
).openapi(
|
|
2092
|
+
createRoute({
|
|
2093
|
+
method: "delete",
|
|
2094
|
+
path: "/charter/root/primitives/{identity}",
|
|
2095
|
+
request: { params: z10.object({ identity: z10.string() }) },
|
|
2096
|
+
responses: {
|
|
2097
|
+
204: { description: "The file is gone" },
|
|
2098
|
+
422: json(DataDTOs.Fault, "A primitive this repository did not author, or none at all")
|
|
2099
|
+
}
|
|
2100
|
+
}),
|
|
2101
|
+
async (c) => {
|
|
2102
|
+
const { identity } = c.req.valid("param");
|
|
2103
|
+
await charterAuthoringApp2.remove(identity);
|
|
2104
|
+
return c.body(null, 204);
|
|
2105
|
+
}
|
|
2106
|
+
).openapi(
|
|
2107
|
+
createRoute({
|
|
2108
|
+
method: "get",
|
|
2109
|
+
path: "/definitions/kinds",
|
|
2110
|
+
responses: { 200: json(DataDTOs.PrimitiveKinds, "Every kind, and when a primitive of it comes up") }
|
|
2111
|
+
}),
|
|
2112
|
+
async (c) => c.json(await charterAuthoringApp2.kinds(), 200)
|
|
2113
|
+
).openapi(
|
|
2114
|
+
createRoute({
|
|
2115
|
+
method: "get",
|
|
2116
|
+
path: "/definitions/kinds/{kind}/requirements",
|
|
2117
|
+
request: { params: z10.object({ kind: z10.string() }) },
|
|
2118
|
+
responses: {
|
|
2119
|
+
200: json(DataDTOs.PrimitiveRequirements, "Every header the kind takes, and its sample"),
|
|
2120
|
+
422: json(DataDTOs.Fault, "A word that is no kind")
|
|
2121
|
+
}
|
|
2122
|
+
}),
|
|
2123
|
+
async (c) => c.json(await charterAuthoringApp2.listPrimitiveRequirements(c.req.valid("param").kind), 200)
|
|
2124
|
+
).openapi(
|
|
2125
|
+
createRoute({
|
|
2126
|
+
method: "get",
|
|
2127
|
+
path: "/charter/root/build",
|
|
2128
|
+
responses: {
|
|
2129
|
+
200: json(DataDTOs.PlanSummary, "What a build would do to every file, nothing written"),
|
|
2130
|
+
422: json(DataDTOs.FaultsByFile, "The charter does not hold")
|
|
2131
|
+
}
|
|
2132
|
+
}),
|
|
2133
|
+
async (c) => {
|
|
2134
|
+
const planSummaryDTO2 = await charterAuthoringApp2.preview();
|
|
2135
|
+
return planSummaryDTO2.type === "FaultsByFile" ? c.json(planSummaryDTO2, 422) : c.json(planSummaryDTO2, 200);
|
|
2136
|
+
}
|
|
2137
|
+
).openapi(
|
|
2138
|
+
createRoute({
|
|
2139
|
+
method: "post",
|
|
2140
|
+
path: "/charter/root/build",
|
|
2141
|
+
responses: {
|
|
2142
|
+
200: json(DataDTOs.PlanSummary, "What the build wrote and deleted"),
|
|
2143
|
+
422: json(DataDTOs.FaultsByFile, "The charter does not hold, and nothing was written")
|
|
2144
|
+
}
|
|
2145
|
+
}),
|
|
2146
|
+
async (c) => {
|
|
2147
|
+
const planSummaryDTO2 = await charterAuthoringApp2.build();
|
|
2148
|
+
return planSummaryDTO2.type === "FaultsByFile" ? c.json(planSummaryDTO2, 422) : c.json(planSummaryDTO2, 200);
|
|
2149
|
+
}
|
|
2150
|
+
).openapi(
|
|
2151
|
+
createRoute({
|
|
2152
|
+
method: "get",
|
|
2153
|
+
path: "/charter/root/health",
|
|
2154
|
+
responses: { 200: json(OutcomeDTOs.DoctorOutcome, "The four answers cw doctor gives, and every fault") }
|
|
2155
|
+
}),
|
|
2156
|
+
async (c) => c.json(await charterAuthoringApp2.doctor(), 200)
|
|
2157
|
+
).openapi(
|
|
2158
|
+
createRoute({
|
|
2159
|
+
method: "get",
|
|
2160
|
+
path: "/test-suites",
|
|
2161
|
+
responses: { 200: json(DataDTOs.TestSuites, "Every test file, its text, and its cases or why it does not read") }
|
|
2162
|
+
}),
|
|
2163
|
+
async (c) => c.json(await testAuthoringApp2.suites(), 200)
|
|
2164
|
+
).openapi(
|
|
2165
|
+
createRoute({
|
|
2166
|
+
method: "post",
|
|
2167
|
+
path: "/test-suites",
|
|
2168
|
+
responses: {
|
|
2169
|
+
201: {
|
|
2170
|
+
...json(z10.string(), "The name of the test file written"),
|
|
2171
|
+
headers: z10.object({ Location: z10.string() })
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
}),
|
|
2175
|
+
async (c) => {
|
|
2176
|
+
const name = await testAuthoringApp2.addSuite();
|
|
2177
|
+
return c.json(name, 201, { Location: `/api/test-suites/${name}` });
|
|
2178
|
+
}
|
|
2179
|
+
).openapi(
|
|
2180
|
+
createRoute({
|
|
2181
|
+
method: "put",
|
|
2182
|
+
path: "/test-suites/{name}",
|
|
2183
|
+
request: {
|
|
2184
|
+
params: z10.object({ name: z10.string() }),
|
|
2185
|
+
body: json(z10.object({ text: z10.string() }), "The whole text the test file is written over with")
|
|
2186
|
+
},
|
|
2187
|
+
responses: {
|
|
2188
|
+
200: json(z10.string(), "The name of the test file written"),
|
|
2189
|
+
422: json(DataDTOs.Fault, "Text that is not a suite, with its sample, or a name that is no test file")
|
|
2190
|
+
}
|
|
2191
|
+
}),
|
|
2192
|
+
async (c) => c.json(await testAuthoringApp2.writeSuite(c.req.valid("param").name, c.req.valid("json").text), 200)
|
|
2193
|
+
).openapi(
|
|
2194
|
+
createRoute({
|
|
2195
|
+
method: "delete",
|
|
2196
|
+
path: "/test-suites/{name}",
|
|
2197
|
+
request: { params: z10.object({ name: z10.string() }) },
|
|
2198
|
+
responses: {
|
|
2199
|
+
204: { description: "The test file is gone" },
|
|
2200
|
+
422: json(DataDTOs.Fault, "A name that is no test file")
|
|
2201
|
+
}
|
|
2202
|
+
}),
|
|
2203
|
+
async (c) => {
|
|
2204
|
+
await testAuthoringApp2.removeSuite(c.req.valid("param").name);
|
|
2205
|
+
return c.body(null, 204);
|
|
2206
|
+
}
|
|
2207
|
+
).openapi(
|
|
2208
|
+
createRoute({
|
|
2209
|
+
method: "get",
|
|
2210
|
+
path: "/test-suites/outcome",
|
|
2211
|
+
responses: {
|
|
2212
|
+
200: json(DataDTOs.TestRunReport, "How every case came out"),
|
|
2213
|
+
422: json(DataDTOs.FaultsByFile, "The charter does not hold, or a test file does not read")
|
|
2214
|
+
}
|
|
2215
|
+
}),
|
|
2216
|
+
async (c) => {
|
|
2217
|
+
const testRunReportDTO2 = await charterAuthoringApp2.test();
|
|
2218
|
+
return testRunReportDTO2.type === "FaultsByFile" ? c.json(testRunReportDTO2, 422) : c.json(testRunReportDTO2, 200);
|
|
2219
|
+
}
|
|
2220
|
+
).openapi(
|
|
2221
|
+
createRoute({
|
|
2222
|
+
method: "get",
|
|
2223
|
+
path: "/vendors",
|
|
2224
|
+
responses: { 200: json(z10.array(z10.string()).readonly(), "Every folder a vendor source was installed as") }
|
|
2225
|
+
}),
|
|
2226
|
+
async (c) => c.json(await charterVendoringApp2.installed(), 200)
|
|
2227
|
+
).openapi(
|
|
2228
|
+
createRoute({
|
|
2229
|
+
method: "post",
|
|
2230
|
+
path: "/vendors",
|
|
2231
|
+
request: {
|
|
2232
|
+
body: json(
|
|
2233
|
+
z10.object({ source: z10.string(), version: z10.string().optional() }),
|
|
2234
|
+
"A source as git fetches it, and the version to pin it to where one is named"
|
|
2235
|
+
)
|
|
2236
|
+
},
|
|
2237
|
+
responses: {
|
|
2238
|
+
204: { description: "The source is installed, and committed" },
|
|
2239
|
+
422: json(DataDTOs.Fault, "No repository, work in hand, or what git refused")
|
|
2240
|
+
}
|
|
2241
|
+
}),
|
|
2242
|
+
async (c) => {
|
|
2243
|
+
const { source, version } = c.req.valid("json");
|
|
2244
|
+
await charterVendoringApp2.add(source, version);
|
|
2245
|
+
return c.body(null, 204);
|
|
2246
|
+
}
|
|
2247
|
+
).openapi(
|
|
2248
|
+
createRoute({
|
|
2249
|
+
method: "delete",
|
|
2250
|
+
path: "/vendors/{name}",
|
|
2251
|
+
request: { params: z10.object({ name: z10.string() }) },
|
|
2252
|
+
responses: {
|
|
2253
|
+
204: { description: "The folder is gone, and that it is gone is committed" },
|
|
2254
|
+
422: json(DataDTOs.Fault, "No repository, work in hand, or what git refused")
|
|
2255
|
+
}
|
|
2256
|
+
}),
|
|
2257
|
+
async (c) => {
|
|
2258
|
+
await charterVendoringApp2.remove(c.req.valid("param").name);
|
|
2259
|
+
return c.body(null, 204);
|
|
2260
|
+
}
|
|
2261
|
+
);
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2264
|
+
// src/driver/portal/server.ts
|
|
2265
|
+
var DEFAULT_PORT = 9927;
|
|
2266
|
+
async function startPortal(page, charterAuthoringApp2, charterVendoringApp2, testAuthoringApp2, port = DEFAULT_PORT) {
|
|
2267
|
+
await access(new URL("index.html", page)).catch(() => {
|
|
2268
|
+
throw new Fault("The portal's page is not built.", 'Run "pnpm build", then start the portal again.');
|
|
2269
|
+
});
|
|
2270
|
+
const token = randomBytes(32).toString("base64url");
|
|
2271
|
+
for (let next = port; ; next++) {
|
|
2272
|
+
try {
|
|
2273
|
+
const address = new URL(`http://127.0.0.1:${next}/?t=${token}`);
|
|
2274
|
+
return { address, server: await listen(portal(page, charterAuthoringApp2, charterVendoringApp2, testAuthoringApp2, token, next), next) };
|
|
2275
|
+
} catch (raised) {
|
|
2276
|
+
if (raised.code !== "EADDRINUSE" || next === 65535) throw raised;
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
function portal(page, charterAuthoringApp2, charterVendoringApp2, testAuthoringApp2, token, port) {
|
|
2281
|
+
const hosts = [`127.0.0.1:${port}`, `localhost:${port}`];
|
|
2282
|
+
return new Hono().use(async (c, next) => {
|
|
2283
|
+
if (!hosts.includes(c.req.header("host") ?? "")) {
|
|
2284
|
+
return c.text(`The portal answers only at ${hosts.join(" and ")}.`, 403);
|
|
2285
|
+
}
|
|
2286
|
+
if (c.req.method !== "GET" && c.req.header("content-type")?.split(";")[0]?.trim() !== "application/json") {
|
|
2287
|
+
return c.text("Every request but a GET is sent with Content-Type: application/json.", 415);
|
|
2288
|
+
}
|
|
2289
|
+
await next();
|
|
2290
|
+
}).use("/", async (c, next) => {
|
|
2291
|
+
if (c.req.query("t") !== token) return c.text('Open the address "cw portal" printed: this one has no token.', 401);
|
|
2292
|
+
await next();
|
|
2293
|
+
}).use("/api/*", bearerAuth({ token })).route("/api", api(charterAuthoringApp2, charterVendoringApp2, testAuthoringApp2)).use("/*", serveStatic({ root: fileURLToPath(page) }));
|
|
2294
|
+
}
|
|
2295
|
+
function listen(app, port) {
|
|
2296
|
+
return new Promise((resolve, reject) => {
|
|
2297
|
+
const server = serve({ fetch: app.fetch, hostname: "127.0.0.1", port }, () => resolve(server));
|
|
2298
|
+
server.once("error", reject);
|
|
2299
|
+
});
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
// src/driver/cli/commands/PortalCommand.ts
|
|
2303
|
+
var OPTIONS6 = {
|
|
2304
|
+
port: {
|
|
2305
|
+
type: "number",
|
|
2306
|
+
describe: "Which port to serve on, the next free one when it is taken",
|
|
2307
|
+
default: DEFAULT_PORT
|
|
2308
|
+
}
|
|
2309
|
+
};
|
|
2310
|
+
var PortalCommand = class {
|
|
2311
|
+
name = "portal";
|
|
2312
|
+
summary = "Serve this repository's charter to a browser on this machine";
|
|
2313
|
+
options = OPTIONS6;
|
|
2314
|
+
async run({ charterAuthoringApp: charterAuthoringApp2, charterVendoringApp: charterVendoringApp2, testAuthoringApp: testAuthoringApp2 }, { port }) {
|
|
2315
|
+
await charterAuthoringApp2.ensureRepoReady();
|
|
2316
|
+
const { address } = await startPortal(
|
|
2317
|
+
new URL("./", import.meta.resolve("#portal/index.html")),
|
|
2318
|
+
charterAuthoringApp2,
|
|
2319
|
+
charterVendoringApp2,
|
|
2320
|
+
testAuthoringApp2,
|
|
2321
|
+
port
|
|
2322
|
+
);
|
|
2323
|
+
return { code: EXIT_OK, result: `The portal is at ${address}
|
|
2324
|
+
Press Ctrl+C to stop it.
|
|
2325
|
+
` };
|
|
2326
|
+
}
|
|
2327
|
+
};
|
|
2328
|
+
|
|
2329
|
+
// src/driver/cli/commands/RemoveCommand.ts
|
|
2330
|
+
import prompts3 from "prompts";
|
|
2331
|
+
var OPTIONS7 = {
|
|
2332
|
+
identity: {
|
|
2333
|
+
type: "string",
|
|
2334
|
+
describe: 'What the primitive is named by across the whole charter: "guide:no-any"'
|
|
2335
|
+
},
|
|
2336
|
+
yes: {
|
|
2337
|
+
type: "boolean",
|
|
2338
|
+
alias: "y",
|
|
2339
|
+
default: false,
|
|
2340
|
+
describe: "Remove it without asking first, as a run with nobody at the terminal must"
|
|
2341
|
+
}
|
|
2342
|
+
};
|
|
2343
|
+
var RemoveCommand = class {
|
|
2344
|
+
name = "remove <identity>";
|
|
2345
|
+
summary = "Delete the file of one primitive this repository authored";
|
|
2346
|
+
options = OPTIONS7;
|
|
2347
|
+
async run({ charterAuthoringApp: charterAuthoringApp2 }, { identity, yes }) {
|
|
2348
|
+
const { scopedPrimitive: scopedPrimitive2 } = (await charterAuthoringApp2.open(identity ?? "")).data;
|
|
2349
|
+
if (!yes && scopedPrimitive2.data.scope === REPO_SCOPE) {
|
|
2350
|
+
if (!process.stdin.isTTY)
|
|
2351
|
+
throw new Fault(
|
|
2352
|
+
`Removing ${scopedPrimitive2.data.file} is asked first, and nobody is at the terminal to answer.`,
|
|
2353
|
+
`Run "cw remove ${scopedPrimitive2.data.identity} --yes" to remove it without asking.`
|
|
2354
|
+
);
|
|
2355
|
+
const { confirmed } = await prompts3({
|
|
2356
|
+
type: "confirm",
|
|
2357
|
+
name: "confirmed",
|
|
2358
|
+
message: `Remove ${scopedPrimitive2.data.file}?`,
|
|
2359
|
+
initial: false
|
|
2360
|
+
});
|
|
2361
|
+
if (confirmed !== true) return { code: EXIT_OK, result: "Nothing was removed.\n" };
|
|
2362
|
+
}
|
|
2363
|
+
const scopedPrimitiveDTO2 = await charterAuthoringApp2.remove(scopedPrimitive2.data.identity);
|
|
2364
|
+
return {
|
|
2365
|
+
code: EXIT_OK,
|
|
2366
|
+
result: [
|
|
2367
|
+
`Removed ${scopedPrimitiveDTO2.data.file}.`,
|
|
2368
|
+
'Run "cw doctor" to see what still names it, then "cw build".',
|
|
2369
|
+
""
|
|
2370
|
+
].join("\n")
|
|
2371
|
+
};
|
|
2372
|
+
}
|
|
2373
|
+
};
|
|
2374
|
+
|
|
2375
|
+
// src/driver/cli/commands/SuiteAddCommand.ts
|
|
2376
|
+
var SuiteAddCommand = class {
|
|
2377
|
+
name = "suite add";
|
|
2378
|
+
summary = `Write a new test file under ${TEST_DIRECTORY}/, holding one sample case`;
|
|
2379
|
+
async run({ testAuthoringApp: testAuthoringApp2 }) {
|
|
2380
|
+
const name = await testAuthoringApp2.addSuite();
|
|
2381
|
+
return {
|
|
2382
|
+
code: EXIT_OK,
|
|
2383
|
+
result: [`Wrote ${TEST_DIRECTORY}/${name}.`, `Run "cw suite edit ${name}" to write its cases, then "cw test".`, ""].join("\n")
|
|
2384
|
+
};
|
|
2385
|
+
}
|
|
2386
|
+
};
|
|
2387
|
+
|
|
2388
|
+
// src/driver/cli/commands/SuiteEditCommand.ts
|
|
2389
|
+
var OPTIONS8 = {
|
|
2390
|
+
name: {
|
|
2391
|
+
type: "string",
|
|
2392
|
+
describe: `The test file's name under ${TEST_DIRECTORY}/: "untitled-1.json"`
|
|
2393
|
+
}
|
|
2394
|
+
};
|
|
2395
|
+
var SuiteEditCommand = class {
|
|
2396
|
+
name = "suite edit <name>";
|
|
2397
|
+
summary = "Open one test file in your editor";
|
|
2398
|
+
options = OPTIONS8;
|
|
2399
|
+
async run({ cwd, testAuthoringApp: testAuthoringApp2 }, { name }) {
|
|
2400
|
+
const { testSuites: testSuites2 } = (await testAuthoringApp2.suites()).data;
|
|
2401
|
+
if (!testSuites2.some((one) => one.data.name === name))
|
|
2402
|
+
throw new Fault(`${TEST_DIRECTORY}/ holds no test file called "${name ?? ""}".`, 'Run "cw suite add" to write a new one.');
|
|
2403
|
+
const file = `${TEST_DIRECTORY}/${name}`;
|
|
2404
|
+
const problem = await editInEditor(cwd, file);
|
|
2405
|
+
if (problem !== void 0) return { code: EXIT_FAILURE, problem };
|
|
2406
|
+
return { code: EXIT_OK, result: [`Closed ${file}.`, 'Run "cw test" to see how its cases come out.', ""].join("\n") };
|
|
2407
|
+
}
|
|
2408
|
+
};
|
|
2409
|
+
|
|
2410
|
+
// src/driver/cli/commands/SuiteRemoveCommand.ts
|
|
2411
|
+
var OPTIONS9 = {
|
|
2412
|
+
name: {
|
|
2413
|
+
type: "string",
|
|
2414
|
+
describe: `The test file's name under ${TEST_DIRECTORY}/: "untitled-1.json"`
|
|
2415
|
+
}
|
|
2416
|
+
};
|
|
2417
|
+
var SuiteRemoveCommand = class {
|
|
2418
|
+
name = "suite remove <name>";
|
|
2419
|
+
summary = "Delete one test file";
|
|
2420
|
+
options = OPTIONS9;
|
|
2421
|
+
async run({ testAuthoringApp: testAuthoringApp2 }, { name }) {
|
|
2422
|
+
const removedName = await testAuthoringApp2.removeSuite(name ?? "");
|
|
2423
|
+
return { code: EXIT_OK, result: `Removed ${TEST_DIRECTORY}/${removedName}.
|
|
2424
|
+
` };
|
|
2425
|
+
}
|
|
2426
|
+
};
|
|
2427
|
+
|
|
2428
|
+
// src/driver/cli/commands/TestCommand.ts
|
|
2429
|
+
var TestCommand = class {
|
|
2430
|
+
name = "test";
|
|
2431
|
+
summary = `Resolve every situation in ${TEST_DIRECTORY}/ against the charter`;
|
|
2432
|
+
async run({ charterAuthoringApp: charterAuthoringApp2 }, _options) {
|
|
2433
|
+
const testRunReportDTO2 = await charterAuthoringApp2.test();
|
|
2434
|
+
if (testRunReportDTO2.type === "FaultsByFile")
|
|
2435
|
+
return { code: EXIT_FAILURE, problem: `Nothing was resolved.
|
|
2436
|
+
|
|
2437
|
+
${toText(testRunReportDTO2)}` };
|
|
2438
|
+
const cases = testRunReportDTO2.data.testCaseReports;
|
|
2439
|
+
if (cases.length === 0) {
|
|
2440
|
+
return {
|
|
2441
|
+
code: EXIT_OK,
|
|
2442
|
+
result: `No tests: ${TEST_DIRECTORY}/ holds no case.
|
|
2443
|
+
Write one as ${TEST_DIRECTORY}/<name>.json to pin a situation down.
|
|
2444
|
+
`
|
|
2445
|
+
};
|
|
2446
|
+
}
|
|
2447
|
+
const failed = cases.filter((one) => !one.data.passed);
|
|
2448
|
+
return {
|
|
2449
|
+
code: failed.length === 0 ? EXIT_OK : EXIT_FAILURE,
|
|
2450
|
+
result: [
|
|
2451
|
+
...cases.flatMap(lines2),
|
|
2452
|
+
"",
|
|
2453
|
+
failed.length === 0 ? `${cases.length} case${cases.length === 1 ? "" : "s"} passed.` : `${failed.length} of ${cases.length} case${cases.length === 1 ? "" : "s"} failed.`,
|
|
2454
|
+
""
|
|
2455
|
+
].join("\n")
|
|
2456
|
+
};
|
|
2457
|
+
}
|
|
2458
|
+
};
|
|
2459
|
+
function lines2(one) {
|
|
2460
|
+
return [
|
|
2461
|
+
` ${one.data.passed ? "pass" : "FAIL"} ${one.data.situation}`,
|
|
2462
|
+
...one.data.unmet === void 0 ? [] : [` ${one.data.unmet.data.message}`, ` ${one.data.unmet.data.fix}`]
|
|
2463
|
+
];
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
// src/driver/cli/commands/VendorAddCommand.ts
|
|
2467
|
+
var OPTIONS10 = {
|
|
2468
|
+
source: {
|
|
2469
|
+
type: "string",
|
|
2470
|
+
describe: "What git clones the charter from: an address over any transport, or a path on disk"
|
|
2471
|
+
},
|
|
2472
|
+
ref: {
|
|
2473
|
+
type: "string",
|
|
2474
|
+
describe: "The tag, branch or commit to pin to; the source's own default where none is named"
|
|
2475
|
+
}
|
|
2476
|
+
};
|
|
2477
|
+
var VendorAddCommand = class {
|
|
2478
|
+
name = "vendor add <source>";
|
|
2479
|
+
summary = "Install a charter from a git source, or bring an installed one up to date";
|
|
2480
|
+
options = OPTIONS10;
|
|
2481
|
+
async run({ charterVendoringApp: charterVendoringApp2 }, { source, ref }) {
|
|
2482
|
+
const folder = await charterVendoringApp2.add(source ?? "", ref);
|
|
2483
|
+
return {
|
|
2484
|
+
code: EXIT_OK,
|
|
2485
|
+
result: [
|
|
2486
|
+
`Installed ${source} under ${folder}${ref === void 0 ? "" : `, pinned to ${ref}`}.`,
|
|
2487
|
+
'Run "cw build" to compile what it adds to this charter.',
|
|
2488
|
+
""
|
|
2489
|
+
].join("\n")
|
|
2490
|
+
};
|
|
2491
|
+
}
|
|
2492
|
+
};
|
|
2493
|
+
|
|
2494
|
+
// src/driver/cli/commands/VendorListCommand.ts
|
|
2495
|
+
var VendorListCommand = class {
|
|
2496
|
+
name = "vendor list";
|
|
2497
|
+
summary = "List the folders this repository installed charters as";
|
|
2498
|
+
async run({ charterVendoringApp: charterVendoringApp2 }) {
|
|
2499
|
+
const folders = await charterVendoringApp2.installed();
|
|
2500
|
+
if (folders.length === 0)
|
|
2501
|
+
return { code: EXIT_OK, result: 'No vendor source is installed. Run "cw vendor add <source>" to install one.\n' };
|
|
2502
|
+
return { code: EXIT_OK, result: `${folders.join("\n")}
|
|
2503
|
+
` };
|
|
2504
|
+
}
|
|
2505
|
+
};
|
|
2506
|
+
|
|
2507
|
+
// src/driver/cli/commands/VendorRemoveCommand.ts
|
|
2508
|
+
var OPTIONS11 = {
|
|
2509
|
+
name: {
|
|
2510
|
+
type: "string",
|
|
2511
|
+
describe: "The folder this charter was installed as, as it reads under .cw/vendor/"
|
|
2512
|
+
}
|
|
2513
|
+
};
|
|
2514
|
+
var VendorRemoveCommand = class {
|
|
2515
|
+
name = "vendor remove <name>";
|
|
2516
|
+
summary = "Take an installed charter away";
|
|
2517
|
+
options = OPTIONS11;
|
|
2518
|
+
async run({ charterVendoringApp: charterVendoringApp2 }, { name }) {
|
|
2519
|
+
const folder = await charterVendoringApp2.remove(name ?? "");
|
|
2520
|
+
return {
|
|
2521
|
+
code: EXIT_OK,
|
|
2522
|
+
result: [
|
|
2523
|
+
`Removed ${folder}.`,
|
|
2524
|
+
'Run "cw build" to compile this charter without it.',
|
|
2525
|
+
""
|
|
2526
|
+
].join("\n")
|
|
2527
|
+
};
|
|
2528
|
+
}
|
|
2529
|
+
};
|
|
2530
|
+
|
|
2531
|
+
// src/driver/cli/commands/index.ts
|
|
2532
|
+
var COMMANDS = [
|
|
2533
|
+
new InitCommand(),
|
|
2534
|
+
new BuildCommand(),
|
|
2535
|
+
new ListCommand(),
|
|
2536
|
+
new KindsCommand(),
|
|
2537
|
+
new AddCommand(),
|
|
2538
|
+
new EditCommand(),
|
|
2539
|
+
new RemoveCommand(),
|
|
2540
|
+
new ExplainCommand(),
|
|
2541
|
+
new DoctorCommand(),
|
|
2542
|
+
new TestCommand(),
|
|
2543
|
+
new SuiteAddCommand(),
|
|
2544
|
+
new SuiteEditCommand(),
|
|
2545
|
+
new SuiteRemoveCommand(),
|
|
2546
|
+
new VendorAddCommand(),
|
|
2547
|
+
new VendorRemoveCommand(),
|
|
2548
|
+
new VendorListCommand(),
|
|
2549
|
+
new PortalCommand()
|
|
2550
|
+
];
|
|
2551
|
+
|
|
2552
|
+
// src/driver/cli/Commander.ts
|
|
2553
|
+
var Commander = class {
|
|
2554
|
+
#context;
|
|
2555
|
+
#commands;
|
|
2556
|
+
constructor(context, commands = COMMANDS) {
|
|
2557
|
+
this.#context = context;
|
|
2558
|
+
this.#commands = commands;
|
|
2559
|
+
}
|
|
2560
|
+
async run(argv) {
|
|
2561
|
+
let code = EXIT_OK;
|
|
2562
|
+
let failed = false;
|
|
2563
|
+
await this.#parser((outcome) => {
|
|
2564
|
+
code = outcome.code;
|
|
2565
|
+
if (outcome.result) process.stdout.write(outcome.result);
|
|
2566
|
+
if (outcome.problem) process.stderr.write(outcome.problem);
|
|
2567
|
+
}).parseAsync([...argv], {}, (error, _parsed, output) => {
|
|
2568
|
+
failed = error !== null && error !== void 0;
|
|
2569
|
+
if (output) (failed ? process.stderr : process.stdout).write(`${output}
|
|
2570
|
+
`);
|
|
2571
|
+
});
|
|
2572
|
+
return failed ? EXIT_USAGE : code;
|
|
2573
|
+
}
|
|
2574
|
+
#parser(collect) {
|
|
2575
|
+
let cli2 = yargs().scriptName("cw").usage("Usage: $0 <command> [options]").demandCommand(1, "").strict().recommendCommands().help("help").alias("h", "help").version(this.#context.version).wrap(80).exitProcess(false);
|
|
2576
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2577
|
+
for (const command of this.#commands) {
|
|
2578
|
+
const head = wordsOf(command.name)[0];
|
|
2579
|
+
groups.set(head, [...groups.get(head) ?? [], command]);
|
|
2580
|
+
}
|
|
2581
|
+
for (const [head, commands] of groups) {
|
|
2582
|
+
const [only] = commands;
|
|
2583
|
+
cli2 = commands.length === 1 && only !== void 0 && wordsOf(only.name).length === 1 ? this.#declaringCommand(cli2, only, only.name, collect) : cli2.command({
|
|
2584
|
+
command: head,
|
|
2585
|
+
// What the group is for is what its commands are: saying it any
|
|
2586
|
+
// other way would be a second description to keep in step.
|
|
2587
|
+
describe: commands.map((one) => wordsOf(one.name)[1]).join(", "),
|
|
2588
|
+
builder: (child) => commands.reduce((parser, command) => this.#declaringCommand(parser, command, command.name.slice(head.length + 1), collect), child).demandCommand(1, ""),
|
|
2589
|
+
handler: () => void 0
|
|
2590
|
+
});
|
|
2591
|
+
}
|
|
2592
|
+
return cli2;
|
|
2593
|
+
}
|
|
2594
|
+
/** One command, said to yargs under the name it is typed by — the whole of it
|
|
2595
|
+
* for a command that stands alone, what follows the group for one inside
|
|
2596
|
+
* it. */
|
|
2597
|
+
#declaringCommand(cli2, command, name, collect) {
|
|
2598
|
+
return cli2.command({
|
|
2599
|
+
command: name,
|
|
2600
|
+
describe: command.summary,
|
|
2601
|
+
builder: (child) => declaringArguments(child, command.options ?? {}, name),
|
|
2602
|
+
handler: async (options) => {
|
|
2603
|
+
try {
|
|
2604
|
+
collect(await command.run(this.#context, options));
|
|
2605
|
+
} catch (raised) {
|
|
2606
|
+
if (!(raised instanceof Fault)) throw raised;
|
|
2607
|
+
collect({ code: EXIT_FAILURE, problem: `${raised.message}
|
|
2608
|
+
${raised.fix}
|
|
2609
|
+
` });
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
});
|
|
2613
|
+
}
|
|
2614
|
+
};
|
|
2615
|
+
function wordsOf(name) {
|
|
2616
|
+
return name.split(" ").filter((word) => !word.startsWith("<") && !word.startsWith("["));
|
|
2617
|
+
}
|
|
2618
|
+
function declaringArguments(cli2, options, name) {
|
|
2619
|
+
return Object.entries(options).reduce((parser, [argument, option]) => {
|
|
2620
|
+
const declared = {
|
|
2621
|
+
type: option.type,
|
|
2622
|
+
describe: option.describe,
|
|
2623
|
+
...option.alias === void 0 ? {} : { alias: option.alias },
|
|
2624
|
+
...option.default === void 0 ? {} : { default: option.default },
|
|
2625
|
+
// Typed once per value: yargs collects them, and a run that typed none
|
|
2626
|
+
// reads an empty list rather than nothing, so the command asks how many
|
|
2627
|
+
// were typed instead of whether any were.
|
|
2628
|
+
...option.isArray === void 0 ? {} : { array: true, default: [] }
|
|
2629
|
+
};
|
|
2630
|
+
return name.includes(`<${argument}>`) || name.includes(`[${argument}]`) ? parser.positional(argument, declared) : parser.option(argument, declared);
|
|
2631
|
+
}, cli2);
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2634
|
+
// src/hexagon/domain/models/charter/builtin/CwAuthorSkill.ts
|
|
2635
|
+
var CwAuthorSkill = class extends SkillPrimitive {
|
|
2636
|
+
constructor() {
|
|
2637
|
+
super(
|
|
2638
|
+
{
|
|
2639
|
+
id: "cw-author",
|
|
2640
|
+
description: "Author a new primitive of this repository's charter, asking cw what its kind requires.",
|
|
2641
|
+
triggers: [
|
|
2642
|
+
"write a rule",
|
|
2643
|
+
"add a standard",
|
|
2644
|
+
"write a skill",
|
|
2645
|
+
"add a command",
|
|
2646
|
+
"author a primitive of the charter"
|
|
2647
|
+
]
|
|
2648
|
+
},
|
|
2649
|
+
`# Authoring a primitive of this charter
|
|
2650
|
+
|
|
2651
|
+
This skill is cw's own. It comes with the engine, is not authored in this
|
|
2652
|
+
repository, and cannot be changed here. A repository that wants another way of
|
|
2653
|
+
authoring writes a primitive of its own, under an identity of its own.
|
|
2654
|
+
|
|
2655
|
+
It covers writing a new primitive. Changing or removing one that already exists
|
|
2656
|
+
is not what it covers.
|
|
2657
|
+
|
|
2658
|
+
Do not write a primitive from memory, or copy the headers of another one. What a
|
|
2659
|
+
kind requires is asked of cw, which answers from the same contract a file is
|
|
2660
|
+
refused against.
|
|
2661
|
+
|
|
2662
|
+
1. If the kind is not settled, run \`cw kinds\`. It names every kind and when a
|
|
2663
|
+
primitive of each comes up; take the one whose line matches what is being
|
|
2664
|
+
asked for.
|
|
2665
|
+
2. Run \`cw kinds <kind>\`. It names every header that kind requires, the shape
|
|
2666
|
+
each takes \u2014 one line, or a list \u2014 the values one drawn from a closed set
|
|
2667
|
+
may hold, and a sample written in full.
|
|
2668
|
+
3. Run \`cw add <kind> <id> --header <name>=<value>\`, one \`--header\` for each
|
|
2669
|
+
header step 2 named, and for any other the sample shows this primitive
|
|
2670
|
+
needs. A header that takes a list is given once per entry, in order; one that
|
|
2671
|
+
takes a line is given once. Everything after the first \`=\` is the value. If
|
|
2672
|
+
cw refuses, it says why and shows the kind's sample: answer again with that
|
|
2673
|
+
in hand. Nothing is written until the headers hold.
|
|
2674
|
+
4. Write the body into the file cw named. The headers decide when the primitive
|
|
2675
|
+
comes up; the body is what an agent reads once it has.
|
|
2676
|
+
5. Run \`cw build\`. If it refuses, \`cw doctor\` names each file with a problem
|
|
2677
|
+
and how to fix it.`
|
|
2678
|
+
);
|
|
2679
|
+
}
|
|
2680
|
+
};
|
|
2681
|
+
|
|
2682
|
+
// src/hexagon/domain/models/charter/builtin/index.ts
|
|
2683
|
+
var BUILTIN_PRIMITIVES = [new CwAuthorSkill()];
|
|
2684
|
+
|
|
2685
|
+
// src/hexagon/service/charterRepo.ts
|
|
2686
|
+
async function loadCharterRoot(repo, fileReaders, yamlParser2) {
|
|
2687
|
+
const repoHref = repo.href.endsWith("/") ? repo.href : `${repo.href}/`;
|
|
2688
|
+
const read = async (layer2) => (await fileReaders.readFilesRecursively(layer2)).filter(
|
|
2689
|
+
({ file }) => file.href.endsWith(".md") && KINDS.some((kind) => file.href.startsWith(`${layer2.href}${kind}/`))
|
|
2690
|
+
).map(({ file, contents }) => ({ path: decodeURIComponent(file.href.slice(repoHref.length)), contents }));
|
|
2691
|
+
const authoredFiles = await read(charterFolderIn(repo));
|
|
2692
|
+
const vendors = await fileReaders.listFolders(vendorFolderIn(repo));
|
|
2693
|
+
const vendorFiles = await Promise.all(
|
|
2694
|
+
vendors.map(async (folder) => {
|
|
2695
|
+
const vendor = decodeURIComponent(folder.href.split("/").at(-2) ?? "");
|
|
2696
|
+
return (await read(folder)).map((one) => ({ ...one, vendor }));
|
|
2697
|
+
})
|
|
2698
|
+
);
|
|
2699
|
+
const builtinFiles = BUILTIN_PRIMITIVES.map((primitive) => ({
|
|
2700
|
+
path: `${BUILTIN_PATH_PREFIX}/${primitive.kind}/${primitive.headers.id}.md`,
|
|
2701
|
+
contents: primitive.toMarkdown()
|
|
2702
|
+
}));
|
|
2703
|
+
return charterRootOf({ repo: authoredFiles, vendor: vendorFiles.flat(), builtin: builtinFiles }, yamlParser2);
|
|
2704
|
+
}
|
|
2705
|
+
async function writeCharter(repo, primitive, fileReader2, fileWriter2) {
|
|
2706
|
+
const under2 = `${primitive.kind}/${primitive.headers.id}.md`;
|
|
2707
|
+
const path = `${CHARTER_DIRECTORY}/${under2}`;
|
|
2708
|
+
const file = new URL(under2, charterFolderIn(repo));
|
|
2709
|
+
if (await fileReader2.readIfThere(file) !== void 0)
|
|
2710
|
+
throw new Fault(
|
|
2711
|
+
`${path} is already there, and writing this one would write over what it holds.`,
|
|
2712
|
+
`Open it, or run this again with an identity this charter has not got.`
|
|
2713
|
+
);
|
|
2714
|
+
await fileWriter2.write(file, primitive.toMarkdown());
|
|
2715
|
+
return new ScopedPrimitive(identityOf({ kind: primitive.kind, id: primitive.headers.id }), REPO_SCOPE, path, primitive);
|
|
2716
|
+
}
|
|
2717
|
+
|
|
2718
|
+
// src/hexagon/domain/models/Settings.ts
|
|
2719
|
+
import { z as z11 } from "zod";
|
|
2720
|
+
var SETTINGS = z11.object({
|
|
2721
|
+
agents: z11.array(z11.enum(AGENT_PROVIDERS))
|
|
2722
|
+
});
|
|
2723
|
+
var WorkspaceSettings = class {
|
|
2724
|
+
constructor(agents) {
|
|
2725
|
+
this.agents = agents;
|
|
2726
|
+
}
|
|
2727
|
+
agents;
|
|
2728
|
+
};
|
|
2729
|
+
|
|
2730
|
+
// src/hexagon/service/settingsRepo.ts
|
|
2731
|
+
async function loadSettings(repo, fileReaders) {
|
|
2732
|
+
const text = await fileReaders.readIfThere(settingsFileIn(repo));
|
|
2733
|
+
if (text === void 0) return new WorkspaceSettings([]);
|
|
2734
|
+
let written;
|
|
2735
|
+
try {
|
|
2736
|
+
written = JSON.parse(text);
|
|
2737
|
+
} catch {
|
|
2738
|
+
throw new SettingsFault(
|
|
2739
|
+
`These are this repository's own settings and this is not JSON.`,
|
|
2740
|
+
`Correct it, or delete it and run "cw init" to write it again.`
|
|
2741
|
+
);
|
|
2742
|
+
}
|
|
2743
|
+
const read = SETTINGS.safeParse(written);
|
|
2744
|
+
if (!read.success) throw whatIsWrongWith(written);
|
|
2745
|
+
return new WorkspaceSettings(read.data.agents);
|
|
2746
|
+
}
|
|
2747
|
+
function whatIsWrongWith(written) {
|
|
2748
|
+
if (typeof written !== "object" || written === null || Array.isArray(written))
|
|
2749
|
+
return new SettingsFault(
|
|
2750
|
+
`These are this repository's own settings and this holds something other than a set of fields.`,
|
|
2751
|
+
`Write it as an object, as in { "agents": ["claude"] }.`
|
|
2752
|
+
);
|
|
2753
|
+
const agents = written.agents;
|
|
2754
|
+
if (!Array.isArray(agents))
|
|
2755
|
+
return new SettingsFault(
|
|
2756
|
+
`"agents" is what this repository compiles its charter for, and it is not a list of names here.`,
|
|
2757
|
+
`Write it as a list of names, as in { "agents": ["claude"] }.`
|
|
2758
|
+
);
|
|
2759
|
+
return new SettingsFault(
|
|
2760
|
+
`This says it compiles for ${agents.filter((one) => !isAgentProvider(one)).map((one) => `"${String(one)}"`).join(", ")}, and this engine compiles for no such agent.`,
|
|
2761
|
+
`Name any of ${AGENT_PROVIDERS.join(", ")}, or drop what is left.`
|
|
2762
|
+
);
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2765
|
+
// src/hexagon/service/testSuitesRepo.ts
|
|
2766
|
+
async function loadTestRoot(repo, fileReaders) {
|
|
2767
|
+
const repoHref = repo.href.endsWith("/") ? repo.href : `${repo.href}/`;
|
|
2768
|
+
return testRootOf(
|
|
2769
|
+
(await fileReaders.readFilesRecursively(testFolderIn(repo))).filter(({ file }) => file.href.endsWith(".json")).map(({ file, contents }) => ({ path: decodeURIComponent(file.href.slice(repoHref.length)), contents }))
|
|
2770
|
+
);
|
|
2771
|
+
}
|
|
2772
|
+
async function addTestSuite(repo, fileReaders, fileWriter2) {
|
|
2773
|
+
const name = (await loadTestRoot(repo, fileReaders)).untitledName;
|
|
2774
|
+
await fileWriter2.write(new URL(name, testFolderIn(repo)), DEFAULT_TEST_SUITE_BODY);
|
|
2775
|
+
return name;
|
|
2776
|
+
}
|
|
2777
|
+
async function writeTestSuite(repo, fileReaders, fileWriter2, name, text) {
|
|
2778
|
+
await ensureTestSuiteNamed(repo, fileReaders, name);
|
|
2779
|
+
testSuiteOf(text);
|
|
2780
|
+
await fileWriter2.write(new URL(name, testFolderIn(repo)), text);
|
|
2781
|
+
return name;
|
|
2782
|
+
}
|
|
2783
|
+
async function removeTestSuite(repo, fileReaders, fileWriter2, name) {
|
|
2784
|
+
await ensureTestSuiteNamed(repo, fileReaders, name);
|
|
2785
|
+
await fileWriter2.delete(new URL(name, testFolderIn(repo)));
|
|
2786
|
+
return name;
|
|
2787
|
+
}
|
|
2788
|
+
async function ensureTestSuiteNamed(repo, fileReaders, name) {
|
|
2789
|
+
if (!(await loadTestRoot(repo, fileReaders)).hasTestSuiteNamed(name))
|
|
2790
|
+
throw new TestSuiteFault(`${TEST_DIRECTORY}/ holds no test file called "${name}".`, 'Run "cw suite add" to write a new one.');
|
|
2791
|
+
}
|
|
2792
|
+
|
|
2793
|
+
// src/hexagon/domain/models/vendor/VendorSource.ts
|
|
2794
|
+
var VendorSource = class {
|
|
2795
|
+
constructor(source, name) {
|
|
2796
|
+
this.source = source;
|
|
2797
|
+
this.name = name;
|
|
2798
|
+
}
|
|
2799
|
+
source;
|
|
2800
|
+
name;
|
|
2801
|
+
};
|
|
2802
|
+
function vendorSourceOf(source) {
|
|
2803
|
+
const name = source.replace(/\.git$/, "").split(/[/:\\]/).filter(Boolean).at(-1) ?? "";
|
|
2804
|
+
if (name === "." || name === ".." || name === "") throw new VendorFault(`Invalid path "${source}".`, "Use correct git path.");
|
|
2805
|
+
return new VendorSource(source, name);
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2808
|
+
// src/hexagon/service/vendorRepo.ts
|
|
2809
|
+
async function loadVendorNames(repo, fileReaders) {
|
|
2810
|
+
return (await fileReaders.listFolders(vendorFolderIn(repo))).map((folder) => `${VENDOR_DIRECTORY}/${decodeURIComponent(folder.href.split("/").at(-2) ?? "")}`).sort();
|
|
2811
|
+
}
|
|
2812
|
+
async function ensureVCSReady(repo, vcs2, because) {
|
|
2813
|
+
if (!await vcs2.isInstalled(repo))
|
|
2814
|
+
throw new Fault(
|
|
2815
|
+
"This folder is not inside a git repository, and a vendor lives in one.",
|
|
2816
|
+
'Run "git init" here, or run this again where your repository is.'
|
|
2817
|
+
);
|
|
2818
|
+
if (!await vcs2.isClean(repo))
|
|
2819
|
+
throw new Fault(
|
|
2820
|
+
`This repository has work in hand, and ${because}.`,
|
|
2821
|
+
"Commit or stash what you are holding, then run this again."
|
|
2822
|
+
);
|
|
2823
|
+
}
|
|
2824
|
+
async function addVendor(repo, vcs2, source, version) {
|
|
2825
|
+
await ensureVCSReady(repo, vcs2, "installing a vendor commits what it installs");
|
|
2826
|
+
const folder = `${VENDOR_DIRECTORY}/${vendorSourceOf(source).name}`;
|
|
2827
|
+
await vcs2.subtreeAdd(source, repo, folder, version);
|
|
2828
|
+
return folder;
|
|
2829
|
+
}
|
|
2830
|
+
async function removeVendor(repo, vcs2, name) {
|
|
2831
|
+
await ensureVCSReady(repo, vcs2, "removing a vendor commits that it is gone");
|
|
2832
|
+
const folder = `${VENDOR_DIRECTORY}/${vendorSourceOf(name).name}`;
|
|
2833
|
+
await vcs2.removeSubFolder(repo, folder);
|
|
2834
|
+
return folder;
|
|
2835
|
+
}
|
|
2836
|
+
async function driftedVendors(repo, vcs2) {
|
|
2837
|
+
const changed = await vcs2.changedUnder(repo, VENDOR_DIRECTORY);
|
|
2838
|
+
const names = changed.map((path) => path.slice(`${VENDOR_DIRECTORY}/`.length).split("/")[0]).filter((name) => name !== void 0 && name !== "");
|
|
2839
|
+
return [...new Set(names)].sort();
|
|
2840
|
+
}
|
|
2841
|
+
|
|
2842
|
+
// src/hexagon/domain/services/testService.ts
|
|
2843
|
+
var TestCaseReport = class {
|
|
2844
|
+
constructor(suiteName, situation, unmet) {
|
|
2845
|
+
this.suiteName = suiteName;
|
|
2846
|
+
this.situation = situation;
|
|
2847
|
+
this.unmet = unmet;
|
|
2848
|
+
}
|
|
2849
|
+
suiteName;
|
|
2850
|
+
situation;
|
|
2851
|
+
unmet;
|
|
2852
|
+
get passed() {
|
|
2853
|
+
return this.unmet === void 0;
|
|
2854
|
+
}
|
|
2855
|
+
};
|
|
2856
|
+
var TestRunReport = class {
|
|
2857
|
+
constructor(testCaseReports) {
|
|
2858
|
+
this.testCaseReports = testCaseReports;
|
|
2859
|
+
}
|
|
2860
|
+
testCaseReports;
|
|
2861
|
+
};
|
|
2862
|
+
function runSuite(charter, suiteName, suite) {
|
|
2863
|
+
return suite.cases.map((one) => runCase(charter, suiteName, one));
|
|
2864
|
+
}
|
|
2865
|
+
function findUntestedPrimitives(charter, testSuites2) {
|
|
2866
|
+
const testedIdentities = new Set(testSuites2.flatMap((testSuite2) => testSuite2.cases.map((one) => one.activatedIdentity)));
|
|
2867
|
+
const faultsByFiles = {};
|
|
2868
|
+
for (const { identity, file, primitive } of charter.primitives) {
|
|
2869
|
+
if (primitive.kind !== "guide" && primitive.kind !== "sensor" || testedIdentities.has(identity)) continue;
|
|
2870
|
+
faultsByFiles[file] = [
|
|
2871
|
+
new TestCaseFault(
|
|
2872
|
+
`No test case names "${identity}", so nothing notices when it stops coming up where it should.`,
|
|
2873
|
+
primitive.kind === "guide" ? `Add a case to a file under .cw/test/: { "do": { "touchFile": "<a file it speaks about>" }, "expect": { "activate": "${identity}" } }.` : `Add a case to a file under .cw/test/: { "when": "${primitive.headers.signal}", "expect": { "run": "${identity}" } }.`,
|
|
2874
|
+
"warn"
|
|
2875
|
+
)
|
|
2876
|
+
];
|
|
2877
|
+
}
|
|
2878
|
+
return new FaultsByFile(faultsByFiles);
|
|
2879
|
+
}
|
|
2880
|
+
function runCase(charter, suiteName, one) {
|
|
2881
|
+
const written = one.written;
|
|
2882
|
+
if ("when" in written) {
|
|
2883
|
+
return new TestCaseReport(suiteName, one.describe(), assertScriptRun(charter, written.when, written.expect.run));
|
|
2884
|
+
}
|
|
2885
|
+
if ("do" in written) {
|
|
2886
|
+
const { touchFile } = written.do;
|
|
2887
|
+
if ("activate" in written.expect) {
|
|
2888
|
+
return new TestCaseReport(suiteName, one.describe(), assertPrimitiveActivated(charter, touchFile, written.expect.activate));
|
|
2889
|
+
}
|
|
2890
|
+
if ("allow" in written.expect) {
|
|
2891
|
+
return new TestCaseReport(suiteName, one.describe(), assertAllowed(charter, touchFile, written.expect.allow));
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
throw new Error(`Never happen`);
|
|
2895
|
+
}
|
|
2896
|
+
function assertPrimitiveActivated(charter, file, identity) {
|
|
2897
|
+
const declared = charter.primitiveById.get(identity);
|
|
2898
|
+
if (declared === void 0) return primitiveNotFound(identity);
|
|
2899
|
+
if (declared.primitive.kind !== "guide")
|
|
2900
|
+
return new TestCaseFault(
|
|
2901
|
+
`Coming up when a file is touched is a guide's rule, and "${identity}" is a ${declared.primitive.kind}.`,
|
|
2902
|
+
`Expect a guide here, or put the situation that kind answers.`
|
|
2903
|
+
);
|
|
2904
|
+
const { globs = [] } = declared.primitive.headers;
|
|
2905
|
+
if (globs.length === 0)
|
|
2906
|
+
return new TestCaseFault(
|
|
2907
|
+
`"${identity}" names no files, so touching one never brings it up.`,
|
|
2908
|
+
`Give that guide "globs", or expect nothing of it here.`
|
|
2909
|
+
);
|
|
2910
|
+
return globs.some((glob) => matches(glob, file)) ? void 0 : new TestCaseFault(
|
|
2911
|
+
`This file matches none of the globs "${identity}" speaks about: ${globs.join(", ")}.`,
|
|
2912
|
+
`Widen that guide's "globs", or touch a file it already speaks about.`
|
|
2913
|
+
);
|
|
2914
|
+
}
|
|
2915
|
+
function assertAllowed(charter, file, expected) {
|
|
2916
|
+
const denying = charter.primitives.filter(
|
|
2917
|
+
({ primitive }) => primitive.kind === "posture" && primitive.headers.deny.some((glob) => matches(glob, file))
|
|
2918
|
+
);
|
|
2919
|
+
if (expected)
|
|
2920
|
+
return denying.length === 0 ? void 0 : new TestCaseFault(
|
|
2921
|
+
`${denying.map((one) => `"${one.identity}"`).join(", ")} denies this file.`,
|
|
2922
|
+
`Drop it from that posture's "deny", or expect "allow": false.`
|
|
2923
|
+
);
|
|
2924
|
+
const postures = charter.primitives.filter(({ primitive }) => primitive.kind === "posture");
|
|
2925
|
+
if (postures.length === 0)
|
|
2926
|
+
return new TestCaseFault(
|
|
2927
|
+
`This charter holds no posture, so it refuses nothing.`,
|
|
2928
|
+
`Author a posture denying this file, or expect "allow": true.`
|
|
2929
|
+
);
|
|
2930
|
+
return denying.length > 0 ? void 0 : new TestCaseFault(
|
|
2931
|
+
`No posture denies this file.`,
|
|
2932
|
+
`Add it to a posture's "deny", or expect "allow": true.`
|
|
2933
|
+
);
|
|
2934
|
+
}
|
|
2935
|
+
function assertScriptRun(charter, event, identity) {
|
|
2936
|
+
const declared = charter.primitiveById.get(identity);
|
|
2937
|
+
if (declared === void 0) return primitiveNotFound(identity);
|
|
2938
|
+
if (declared.primitive.kind !== "sensor")
|
|
2939
|
+
return new TestCaseFault(
|
|
2940
|
+
`Running when an event is raised is a sensor's rule, and "${identity}" is a ${declared.primitive.kind}.`,
|
|
2941
|
+
`Expect a sensor here, or put the situation that kind answers.`
|
|
2942
|
+
);
|
|
2943
|
+
const { signal } = declared.primitive.headers;
|
|
2944
|
+
return signal === event ? void 0 : new TestCaseFault(
|
|
2945
|
+
`"${identity}" fires on ${signal}, not on ${event}.`,
|
|
2946
|
+
`Raise the event it names, or declare the "signal" this case raises.`
|
|
2947
|
+
);
|
|
2948
|
+
}
|
|
2949
|
+
function primitiveNotFound(identity) {
|
|
2950
|
+
return new TestCaseFault(
|
|
2951
|
+
`This charter holds no primitive called "${identity}".`,
|
|
2952
|
+
`Author it, or correct the name. "cw list --min" says every identity this charter does hold.`
|
|
2953
|
+
);
|
|
2954
|
+
}
|
|
2955
|
+
|
|
2956
|
+
// src/hexagon/domain/specifications/RepoScopedPrimitive.ts
|
|
2957
|
+
var RepoScopedPrimitive = class {
|
|
2958
|
+
static isSatisfiedBy(scopedPrimitive2) {
|
|
2959
|
+
return scopedPrimitive2.scope === REPO_SCOPE;
|
|
2960
|
+
}
|
|
2961
|
+
};
|
|
2962
|
+
|
|
2963
|
+
// src/hexagon/application/helper.ts
|
|
2964
|
+
async function contentHashOf(content) {
|
|
2965
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(content));
|
|
2966
|
+
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
2967
|
+
}
|
|
2968
|
+
|
|
2969
|
+
// src/hexagon/domain/models/output/StampedDocument.ts
|
|
2970
|
+
var GENERATED_STAMP = "Generated by cherry-works";
|
|
2971
|
+
function stamp(document) {
|
|
2972
|
+
return `${document.replace(/\n+$/, "")}
|
|
2973
|
+
|
|
2974
|
+
<!-- ${GENERATED_STAMP}. Do not edit; edit the charter and build again. -->
|
|
2975
|
+
`;
|
|
2976
|
+
}
|
|
2977
|
+
function isStamped(contents) {
|
|
2978
|
+
return contents.includes(GENERATED_STAMP);
|
|
2979
|
+
}
|
|
2980
|
+
|
|
2981
|
+
// src/hexagon/domain/models/output/CharterOutput.ts
|
|
2982
|
+
var Catalogue = class _Catalogue {
|
|
2983
|
+
constructor(entries) {
|
|
2984
|
+
this.entries = entries;
|
|
2985
|
+
}
|
|
2986
|
+
entries;
|
|
2987
|
+
static of(entries) {
|
|
2988
|
+
return new _Catalogue(
|
|
2989
|
+
[...entries].sort((one, another) => one.identity < another.identity ? -1 : one.identity > another.identity ? 1 : 0)
|
|
2990
|
+
);
|
|
2991
|
+
}
|
|
2992
|
+
/** This listing is the charter's own file: it says what the charter holds now,
|
|
2993
|
+
* and nothing else has anything to say in it. */
|
|
2994
|
+
projection = "replace";
|
|
2995
|
+
/**
|
|
2996
|
+
* The same listing, narrowed to the primitives of one kind (FR-011).
|
|
2997
|
+
*
|
|
2998
|
+
* A narrowed listing is a listing: what comes back answers `full` and
|
|
2999
|
+
* `compact` the way this does, so whoever reads it never has to ask whether it
|
|
3000
|
+
* was narrowed. Nothing is re-sorted — the entries keep the order they were
|
|
3001
|
+
* put in, which is the order by identity `of` gave them.
|
|
3002
|
+
*/
|
|
3003
|
+
filterByKind(kind) {
|
|
3004
|
+
return new _Catalogue(this.entries.filter((one) => one.kind === kind));
|
|
3005
|
+
}
|
|
3006
|
+
/** Everything every primitive's headers declare about it, and the file its body
|
|
3007
|
+
* is in (FR-011). */
|
|
3008
|
+
get full() {
|
|
3009
|
+
return this.entries;
|
|
3010
|
+
}
|
|
3011
|
+
/**
|
|
3012
|
+
* The same primitives as an agent surveys them: what each is, what it is
|
|
3013
|
+
* called, and the one line saying what it is for (FR-012).
|
|
3014
|
+
*
|
|
3015
|
+
* This is what an agent reads first, so it holds nothing an agent surveying the
|
|
3016
|
+
* charter would not use — no globs, no tags, and no file. Having found what it
|
|
3017
|
+
* wants here, it asks `full` where that identity lives.
|
|
3018
|
+
*/
|
|
3019
|
+
get compact() {
|
|
3020
|
+
return this.entries.map(({ identity, kind, id, description }) => ({ identity, kind, id, description }));
|
|
3021
|
+
}
|
|
3022
|
+
};
|
|
3023
|
+
var CharterMd = class _CharterMd {
|
|
3024
|
+
constructor(body) {
|
|
3025
|
+
this.body = body;
|
|
3026
|
+
}
|
|
3027
|
+
body;
|
|
3028
|
+
static type = "charter-md";
|
|
3029
|
+
type = _CharterMd.type;
|
|
3030
|
+
static of(kinds) {
|
|
3031
|
+
return new _CharterMd(`${[
|
|
3032
|
+
"# Charter",
|
|
3033
|
+
`This repository is governed by a charter \u2014 the standards it authored under \`${CHARTER_DIRECTORY}/\`, which constrain whatever coding agent runs here. Every surface an agent reads, this file included, is generated from that charter: do not edit them, edit the primitive behind them.`,
|
|
3034
|
+
"## Read first",
|
|
3035
|
+
`[catalog.min.json](./catalog.min.json) \u2014 the kind, identity and description of every primitive this charter holds. Survey it to find what you need; open [catalog.json](./catalog.json) for that primitive's file, globs and mixins, and open a body only once its activation condition below is met.`,
|
|
3036
|
+
"## When each kind applies",
|
|
3037
|
+
kinds.map((one) => `- **${one.kind}** \u2014 ${one.activatesWhen}.`).join("\n"),
|
|
3038
|
+
"## One identity, one primitive",
|
|
3039
|
+
`An identity names one primitive in the whole charter: \`kind:id\`, whether this repository authored it, installed it from a vendor, or the engine brought it. Nothing overrides anything \u2014 two files claiming one identity is an error the charter refuses to build with.`
|
|
3040
|
+
].join("\n\n")}
|
|
3041
|
+
`);
|
|
3042
|
+
}
|
|
3043
|
+
/** This file is the charter's own: it is written whole over whatever is
|
|
3044
|
+
* there. */
|
|
3045
|
+
projection = "replace";
|
|
3046
|
+
/** This file as a reader opens it: the stamp saying this engine wrote it, and
|
|
3047
|
+
* the orientation itself. */
|
|
3048
|
+
toStampedDocument() {
|
|
3049
|
+
return stamp(this.body);
|
|
3050
|
+
}
|
|
3051
|
+
};
|
|
3052
|
+
|
|
3053
|
+
// src/hexagon/domain/models/output/providers/claude/ClaudeSettingsComponent.ts
|
|
3054
|
+
var CLAUDE_HOOK_EVENTS = [
|
|
3055
|
+
"PreToolUse",
|
|
3056
|
+
"PostToolUse",
|
|
3057
|
+
"UserPromptSubmit",
|
|
3058
|
+
"Notification",
|
|
3059
|
+
"Stop",
|
|
3060
|
+
"SubagentStop",
|
|
3061
|
+
"PreCompact",
|
|
3062
|
+
"SessionStart",
|
|
3063
|
+
"SessionEnd"
|
|
3064
|
+
];
|
|
3065
|
+
function isClaudeHookEvent(name) {
|
|
3066
|
+
return CLAUDE_HOOK_EVENTS.includes(name);
|
|
3067
|
+
}
|
|
3068
|
+
var ClaudeSettingsComponent = class _ClaudeSettingsComponent {
|
|
3069
|
+
constructor(settings) {
|
|
3070
|
+
this.settings = settings;
|
|
3071
|
+
}
|
|
3072
|
+
settings;
|
|
3073
|
+
static kind = "settings";
|
|
3074
|
+
kind = _ClaudeSettingsComponent.kind;
|
|
3075
|
+
/** The settings file this host reads is shared with whatever the repository set
|
|
3076
|
+
* for itself, so what is put down there is written into what is there rather
|
|
3077
|
+
* than in place of it (FR-018). */
|
|
3078
|
+
projection = "mergeJSON";
|
|
3079
|
+
static of(settings) {
|
|
3080
|
+
return new _ClaudeSettingsComponent(settings);
|
|
3081
|
+
}
|
|
3082
|
+
};
|
|
3083
|
+
|
|
3084
|
+
// src/hexagon/domain/models/output/providers/claude/document-based-components/DocumentBasedComponent.ts
|
|
3085
|
+
var DocumentBasedComponent = class {
|
|
3086
|
+
constructor(name, headers, body) {
|
|
3087
|
+
this.name = name;
|
|
3088
|
+
this.headers = headers;
|
|
3089
|
+
this.body = body;
|
|
3090
|
+
}
|
|
3091
|
+
name;
|
|
3092
|
+
headers;
|
|
3093
|
+
body;
|
|
3094
|
+
/** A document is this primitive's alone, so putting it down replaces whatever
|
|
3095
|
+
* is there (FR-020). */
|
|
3096
|
+
projection = "replace";
|
|
3097
|
+
/**
|
|
3098
|
+
* This document as the file this host reads: its frontmatter, its body, and
|
|
3099
|
+
* the stamp saying this engine wrote it.
|
|
3100
|
+
*
|
|
3101
|
+
* Called where a projection is put on disk. A kind that declares no frontmatter
|
|
3102
|
+
* gets none.
|
|
3103
|
+
*
|
|
3104
|
+
* Every one of them carries the stamp, which is what a reader is told and what
|
|
3105
|
+
* a build reads back before taking a file away — a document without it is
|
|
3106
|
+
* someone's own and is left alone (FR-017, FR-020).
|
|
3107
|
+
*/
|
|
3108
|
+
toStampedDocument() {
|
|
3109
|
+
const headers = Object.entries(this.headers);
|
|
3110
|
+
return stamp(
|
|
3111
|
+
[
|
|
3112
|
+
// A kind whose file this host reads whole declares no frontmatter, and
|
|
3113
|
+
// gets none: an empty block would be this engine writing a field for
|
|
3114
|
+
// this host to read, and there is none to write.
|
|
3115
|
+
...headers.length === 0 ? [] : [["---", ...headers.map(([field, value]) => `${field}: ${formatFrontmatterValue(value)}`), "---"].join("\n")],
|
|
3116
|
+
this.body
|
|
3117
|
+
].filter((block) => block !== "").join("\n\n")
|
|
3118
|
+
);
|
|
3119
|
+
}
|
|
3120
|
+
};
|
|
3121
|
+
|
|
3122
|
+
// src/hexagon/domain/models/output/providers/claude/document-based-components/ClaudeAgentComponent.ts
|
|
3123
|
+
var ClaudeAgentComponent = class _ClaudeAgentComponent extends DocumentBasedComponent {
|
|
3124
|
+
static kind = "agent";
|
|
3125
|
+
kind = _ClaudeAgentComponent.kind;
|
|
3126
|
+
static of(name, declared, body) {
|
|
3127
|
+
return new _ClaudeAgentComponent(name, { name, ...declared }, body);
|
|
3128
|
+
}
|
|
3129
|
+
};
|
|
3130
|
+
|
|
3131
|
+
// src/hexagon/domain/models/output/providers/claude/document-based-components/ClaudeCommandComponent.ts
|
|
3132
|
+
var ClaudeCommandComponent = class _ClaudeCommandComponent extends DocumentBasedComponent {
|
|
3133
|
+
static kind = "command";
|
|
3134
|
+
kind = _ClaudeCommandComponent.kind;
|
|
3135
|
+
static of(name, headers, body) {
|
|
3136
|
+
return new _ClaudeCommandComponent(name, headers, body);
|
|
3137
|
+
}
|
|
3138
|
+
};
|
|
3139
|
+
|
|
3140
|
+
// src/hexagon/domain/models/output/providers/claude/document-based-components/ClaudeRuleComponent.ts
|
|
3141
|
+
var ClaudeRuleComponent = class _ClaudeRuleComponent extends DocumentBasedComponent {
|
|
3142
|
+
static kind = "rule";
|
|
3143
|
+
kind = _ClaudeRuleComponent.kind;
|
|
3144
|
+
static of(name, headers, body) {
|
|
3145
|
+
return new _ClaudeRuleComponent(name, headers, body);
|
|
3146
|
+
}
|
|
3147
|
+
};
|
|
3148
|
+
|
|
3149
|
+
// src/hexagon/domain/models/output/providers/claude/document-based-components/ClaudeSkillComponent.ts
|
|
3150
|
+
var ClaudeSkillComponent = class _ClaudeSkillComponent extends DocumentBasedComponent {
|
|
3151
|
+
static kind = "skill";
|
|
3152
|
+
kind = _ClaudeSkillComponent.kind;
|
|
3153
|
+
static of(name, declared, body) {
|
|
3154
|
+
return new _ClaudeSkillComponent(name, { name, ...declared }, body);
|
|
3155
|
+
}
|
|
3156
|
+
};
|
|
3157
|
+
|
|
3158
|
+
// src/hexagon/domain/services/compileService.ts
|
|
3159
|
+
function compile(charter, agents) {
|
|
3160
|
+
return {
|
|
3161
|
+
// A header nobody wrote is left out rather than listed as nothing: a
|
|
3162
|
+
// listing says what the author declared. `file` is where the body sits
|
|
3163
|
+
// inside the workspace — a vendored primitive reads as
|
|
3164
|
+
// `.cw/vendor/<name>/...`, which is where it is.
|
|
3165
|
+
catalogue: Catalogue.of(
|
|
3166
|
+
charter.primitives.map((one) => {
|
|
3167
|
+
const { id, description, tags, globs, rationale, mixins } = one.primitive.headers;
|
|
3168
|
+
return {
|
|
3169
|
+
identity: one.identity,
|
|
3170
|
+
kind: one.primitive.kind,
|
|
3171
|
+
id,
|
|
3172
|
+
description,
|
|
3173
|
+
file: one.file,
|
|
3174
|
+
...tags === void 0 ? {} : { tags },
|
|
3175
|
+
...globs === void 0 ? {} : { globs },
|
|
3176
|
+
...rationale === void 0 ? {} : { rationale },
|
|
3177
|
+
...mixins === void 0 ? {} : { mixins }
|
|
3178
|
+
};
|
|
3179
|
+
})
|
|
3180
|
+
),
|
|
3181
|
+
charterMd: CharterMd.of(PRIMITIVE_CLASSES),
|
|
3182
|
+
providerComponents: agents.flatMap((agent) => forAgent(agent, charter))
|
|
3183
|
+
};
|
|
3184
|
+
}
|
|
3185
|
+
function forAgent(agent, charter) {
|
|
3186
|
+
switch (agent) {
|
|
3187
|
+
case "claude":
|
|
3188
|
+
return compileClaude(charter);
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
function compileClaude(charter) {
|
|
3192
|
+
const asSettings = charter.primitives.filter(isSettingComponent);
|
|
3193
|
+
const asDocuments = charter.primitives.filter((one) => !isSettingComponent(one));
|
|
3194
|
+
return [
|
|
3195
|
+
...asSettings.length === 0 ? [] : [ClaudeSettingsComponent.of(claudeSettingsOf(asSettings))],
|
|
3196
|
+
...asDocuments.flatMap((one) => claudeDocumentComponentOf(charter, one) ?? [])
|
|
3197
|
+
];
|
|
3198
|
+
}
|
|
3199
|
+
function isSettingComponent(one) {
|
|
3200
|
+
return one.primitive.kind === PosturePrimitive.kind || one.primitive.kind === SensorPrimitive.kind;
|
|
3201
|
+
}
|
|
3202
|
+
function claudeSettingsOf(ones) {
|
|
3203
|
+
const allow = /* @__PURE__ */ new Set();
|
|
3204
|
+
const deny = /* @__PURE__ */ new Set();
|
|
3205
|
+
const hooks = {};
|
|
3206
|
+
for (const one of ones) {
|
|
3207
|
+
if (one.primitive.kind === PosturePrimitive.kind) {
|
|
3208
|
+
const headers = one.primitive.headers;
|
|
3209
|
+
for (const permission of headers.allow) allow.add(permission);
|
|
3210
|
+
for (const permission of headers.deny) deny.add(permission);
|
|
3211
|
+
}
|
|
3212
|
+
if (one.primitive.kind === SensorPrimitive.kind) {
|
|
3213
|
+
const { signal, run: run2 } = one.primitive.headers;
|
|
3214
|
+
if (isClaudeHookEvent(signal))
|
|
3215
|
+
hooks[signal] = [...hooks[signal] ?? [], { hooks: [{ type: "command", command: run2 }] }];
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
return {
|
|
3219
|
+
...allow.size === 0 && deny.size === 0 ? {} : { permissions: { allow: [...allow], deny: [...deny] } },
|
|
3220
|
+
...Object.keys(hooks).length === 0 ? {} : { hooks }
|
|
3221
|
+
};
|
|
3222
|
+
}
|
|
3223
|
+
function claudeDocumentComponentOf(charter, sc) {
|
|
3224
|
+
const name = claudeNameOf(sc);
|
|
3225
|
+
const body = charter.bodyOf(sc);
|
|
3226
|
+
switch (sc.primitive.kind) {
|
|
3227
|
+
// A guide is a rule this host loads into context whole: when a file it names
|
|
3228
|
+
// is touched, which is what `paths` on a rule does, and at the start of every
|
|
3229
|
+
// session where it names none (FR-013). Under the identity it is changed by,
|
|
3230
|
+
// so a reader who wants it changed is sent to the primitive rather than
|
|
3231
|
+
// editing what the next build overwrites (FR-017, FR-020).
|
|
3232
|
+
case GuidePrimitive.kind: {
|
|
3233
|
+
const { globs = [] } = sc.primitive.headers;
|
|
3234
|
+
return ClaudeRuleComponent.of(
|
|
3235
|
+
name,
|
|
3236
|
+
globs.length === 0 ? {} : { paths: globs },
|
|
3237
|
+
`${[`## ${sc.identity}`, body].join("\n\n")}
|
|
3238
|
+
`
|
|
3239
|
+
);
|
|
3240
|
+
}
|
|
3241
|
+
// Named by the identifier alone, because the filename is what someone types
|
|
3242
|
+
// after the slash: this host keeps commands in a directory of their own, so
|
|
3243
|
+
// no other kind can land beside one and the kind would only be typed twice.
|
|
3244
|
+
case CommandPrimitive.kind:
|
|
3245
|
+
return ClaudeCommandComponent.of(sc.primitive.headers.id, { description: sc.primitive.description() }, body);
|
|
3246
|
+
case AgentPrimitive.kind: {
|
|
3247
|
+
const { tools } = sc.primitive.headers;
|
|
3248
|
+
return ClaudeAgentComponent.of(name, { description: sc.primitive.description(), tools: tools.join(", ") }, body);
|
|
3249
|
+
}
|
|
3250
|
+
// One kind of this host for the two the charter loads when the request calls
|
|
3251
|
+
// for them: what decides that the body is worth opening is the description,
|
|
3252
|
+
// because that line is all this host reads before deciding (FR-013), and
|
|
3253
|
+
// composing it is the primitive's.
|
|
3254
|
+
case SkillPrimitive.kind:
|
|
3255
|
+
case PlaybookPrimitive.kind:
|
|
3256
|
+
return ClaudeSkillComponent.of(name, { description: sc.primitive.description() }, body);
|
|
3257
|
+
default:
|
|
3258
|
+
return void 0;
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
3261
|
+
function claudeNameOf(one) {
|
|
3262
|
+
return one.identity.replace(/[:/]/g, "-");
|
|
3263
|
+
}
|
|
3264
|
+
|
|
3265
|
+
// src/hexagon/domain/services/projectionService.ts
|
|
3266
|
+
function charterOutputProjection(output, agentProviders) {
|
|
3267
|
+
return [
|
|
3268
|
+
// Indented: this one is committed and read in diffs, and a listing whose
|
|
3269
|
+
// entries land one per line says what changed in the charter rather than
|
|
3270
|
+
// that the listing changed.
|
|
3271
|
+
{
|
|
3272
|
+
path: `${OUT_DIRECTORY}/catalog.json`,
|
|
3273
|
+
contents: `${JSON.stringify(output.catalogue.full, void 0, 2)}
|
|
3274
|
+
`,
|
|
3275
|
+
projectionPolicy: output.catalogue.projection
|
|
3276
|
+
},
|
|
3277
|
+
// Not indented: this one is read to be surveyed cheaply, and the whitespace
|
|
3278
|
+
// is bytes an agent pays for and no one reads (SC-005).
|
|
3279
|
+
{
|
|
3280
|
+
path: `${OUT_DIRECTORY}/catalog.min.json`,
|
|
3281
|
+
contents: `${JSON.stringify(output.catalogue.compact)}
|
|
3282
|
+
`,
|
|
3283
|
+
projectionPolicy: output.catalogue.projection
|
|
3284
|
+
},
|
|
3285
|
+
{
|
|
3286
|
+
path: `${OUT_DIRECTORY}/CHARTER.md`,
|
|
3287
|
+
contents: output.charterMd.toStampedDocument(),
|
|
3288
|
+
projectionPolicy: output.charterMd.projection
|
|
3289
|
+
},
|
|
3290
|
+
...agentProviders.map(entryFileProjection),
|
|
3291
|
+
...output.providerComponents.map(componentProjection)
|
|
3292
|
+
];
|
|
3293
|
+
}
|
|
3294
|
+
function entryFileProjection(provider) {
|
|
3295
|
+
const contents = `<!-- CHERRYWORKS START -->
|
|
3296
|
+
${[
|
|
3297
|
+
"## Charter",
|
|
3298
|
+
`This repository is governed by a charter. Read [CHARTER.md](./${OUT_DIRECTORY}/CHARTER.md) before anything else here, and work under what it says.`,
|
|
3299
|
+
"It is generated from the charter under `.cw/charter/`: do not edit it, edit the primitive behind it."
|
|
3300
|
+
].join("\n\n")}
|
|
3301
|
+
<!-- CHERRYWORKS END -->
|
|
3302
|
+
`;
|
|
3303
|
+
let path;
|
|
3304
|
+
switch (provider) {
|
|
3305
|
+
case "claude":
|
|
3306
|
+
path = CLAUDE_ENTRY_FILE;
|
|
3307
|
+
break;
|
|
3308
|
+
}
|
|
3309
|
+
return { path, contents, projectionPolicy: "upsertWithMarker" };
|
|
3310
|
+
}
|
|
3311
|
+
function componentProjection(one) {
|
|
3312
|
+
const projectionPolicy = one.projection;
|
|
3313
|
+
switch (one.kind) {
|
|
3314
|
+
case ClaudeCommandComponent.kind:
|
|
3315
|
+
return { path: `${CLAUDE_DIRECTORY}/commands/${one.name}.md`, contents: one.toStampedDocument(), projectionPolicy };
|
|
3316
|
+
case ClaudeAgentComponent.kind:
|
|
3317
|
+
return { path: `${CLAUDE_DIRECTORY}/agents/${one.name}.md`, contents: one.toStampedDocument(), projectionPolicy };
|
|
3318
|
+
// A skill is a directory holding one file of that name, which is where this
|
|
3319
|
+
// host looks for it.
|
|
3320
|
+
case ClaudeSkillComponent.kind:
|
|
3321
|
+
return {
|
|
3322
|
+
path: `${CLAUDE_DIRECTORY}/skills/${one.name}/SKILL.md`,
|
|
3323
|
+
contents: one.toStampedDocument(),
|
|
3324
|
+
projectionPolicy
|
|
3325
|
+
};
|
|
3326
|
+
// A rule of its own file, under the directory this host loads every one of:
|
|
3327
|
+
// at the start of a session where it declares no `paths`, and when one of
|
|
3328
|
+
// them is touched where it does.
|
|
3329
|
+
case ClaudeRuleComponent.kind:
|
|
3330
|
+
return { path: `${CLAUDE_DIRECTORY}/rules/${one.name}.md`, contents: one.toStampedDocument(), projectionPolicy };
|
|
3331
|
+
// Every posture and every sensor of the charter lands in the one file this
|
|
3332
|
+
// host reads its settings from.
|
|
3333
|
+
case ClaudeSettingsComponent.kind:
|
|
3334
|
+
return {
|
|
3335
|
+
path: `${CLAUDE_DIRECTORY}/settings.json`,
|
|
3336
|
+
contents: `${JSON.stringify(one.settings, void 0, 2)}
|
|
3337
|
+
`,
|
|
3338
|
+
projectionPolicy
|
|
3339
|
+
};
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
|
|
3343
|
+
// src/hexagon/service/buildService.ts
|
|
3344
|
+
async function plan(repo, charter, agentProviders, fileReaders) {
|
|
3345
|
+
const [cleanupPlan, projectionPlan] = await Promise.all([
|
|
3346
|
+
planForCleanup(repo, agentProviders, fileReaders),
|
|
3347
|
+
planForProjection(repo, compile(charter, agentProviders), agentProviders, fileReaders)
|
|
3348
|
+
]);
|
|
3349
|
+
return { cleanupPlan, projectionPlan };
|
|
3350
|
+
}
|
|
3351
|
+
async function planForProjection(repo, output, agentProviders, fileReaders) {
|
|
3352
|
+
const planned = /* @__PURE__ */ new Map();
|
|
3353
|
+
for (const one of charterOutputProjection(output, agentProviders)) {
|
|
3354
|
+
const file = new URL(one.path, repo);
|
|
3355
|
+
const held = await fileReaders.readIfThere(file);
|
|
3356
|
+
let contents;
|
|
3357
|
+
switch (one.projectionPolicy) {
|
|
3358
|
+
case "replace":
|
|
3359
|
+
contents = one.contents;
|
|
3360
|
+
break;
|
|
3361
|
+
// What the repository set for itself is read and written back: the fields
|
|
3362
|
+
// this charter speaks for are put over the top of it, and every other one
|
|
3363
|
+
// is theirs and comes through untouched (FR-018).
|
|
3364
|
+
case "mergeJSON": {
|
|
3365
|
+
const already = JSON.parse(held?.trim() || "{}");
|
|
3366
|
+
contents = `${JSON.stringify({ ...already, ...JSON.parse(one.contents) }, void 0, 2)}
|
|
3367
|
+
`;
|
|
3368
|
+
break;
|
|
3369
|
+
}
|
|
3370
|
+
// A section says where it starts and ends itself — its first line and its
|
|
3371
|
+
// last — so nothing here knows what a marker of this engine's looks like,
|
|
3372
|
+
// and a second output written this way brings its own (FR-051).
|
|
3373
|
+
case "upsertWithMarker": {
|
|
3374
|
+
const already = held ?? "";
|
|
3375
|
+
const lines3 = one.contents.trimEnd().split("\n");
|
|
3376
|
+
const [start, end] = [lines3.at(0) ?? "", lines3.at(-1) ?? ""].map(
|
|
3377
|
+
(line) => line.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
3378
|
+
);
|
|
3379
|
+
const section = new RegExp(`${start}[\\s\\S]*?${end}\\n?`, "g");
|
|
3380
|
+
if (already.match(section) === null) {
|
|
3381
|
+
contents = already === "" ? one.contents : `${already.replace(/\n*$/, "\n")}
|
|
3382
|
+
${one.contents}`;
|
|
3383
|
+
break;
|
|
3384
|
+
}
|
|
3385
|
+
let written = false;
|
|
3386
|
+
contents = already.replace(section, () => {
|
|
3387
|
+
if (written) return "";
|
|
3388
|
+
written = true;
|
|
3389
|
+
return one.contents;
|
|
3390
|
+
});
|
|
3391
|
+
break;
|
|
3392
|
+
}
|
|
3393
|
+
}
|
|
3394
|
+
planned.set(one.path, { path: one.path, file, contents, held });
|
|
3395
|
+
}
|
|
3396
|
+
return [...planned.values()];
|
|
3397
|
+
}
|
|
3398
|
+
async function planForCleanup(repo, agentProviders, fileReaders) {
|
|
3399
|
+
const outFolder = outFolderIn(repo);
|
|
3400
|
+
const folders = [outFolder, ...agentProviders.map((agent) => agentProviderFolderIn(repo, agent))];
|
|
3401
|
+
const found = await Promise.all(folders.map((folder) => fileReaders.readFilesRecursively(folder)));
|
|
3402
|
+
return found.flat().filter(({ file, contents }) => file.href.startsWith(outFolder.href) || isStamped(contents)).map(({ file, contents }) => ({ path: decodeURIComponent(file.href.slice(repo.href.length)), file, held: contents }));
|
|
3403
|
+
}
|
|
3404
|
+
var PlanSummary = class {
|
|
3405
|
+
constructor(added, edited, deleted, unchanged) {
|
|
3406
|
+
this.added = added;
|
|
3407
|
+
this.edited = edited;
|
|
3408
|
+
this.deleted = deleted;
|
|
3409
|
+
this.unchanged = unchanged;
|
|
3410
|
+
}
|
|
3411
|
+
added;
|
|
3412
|
+
edited;
|
|
3413
|
+
deleted;
|
|
3414
|
+
unchanged;
|
|
3415
|
+
};
|
|
3416
|
+
function previewPlan(cleanupPlan, projectionPlan) {
|
|
3417
|
+
return new PlanSummary(
|
|
3418
|
+
projectionPlan.filter((one) => one.held === void 0).map((one) => one.path),
|
|
3419
|
+
projectionPlan.filter((one) => one.held !== void 0 && one.held !== one.contents).map((one) => one.path),
|
|
3420
|
+
cleanupPlan.filter((one) => !projectionPlan.some((other) => other.path === one.path)).map((one) => one.path),
|
|
3421
|
+
projectionPlan.filter((one) => one.held === one.contents).map((one) => one.path)
|
|
3422
|
+
);
|
|
3423
|
+
}
|
|
3424
|
+
async function executePlan(cleanupPlan, projectionPlan, fileWriter2) {
|
|
3425
|
+
try {
|
|
3426
|
+
await Promise.all(cleanupPlan.map((one) => fileWriter2.delete(one.file)));
|
|
3427
|
+
for (const one of projectionPlan) await fileWriter2.write(one.file, one.contents ?? "");
|
|
3428
|
+
} catch (raised) {
|
|
3429
|
+
throw new Fault(
|
|
3430
|
+
`This build could not be written: ${raised instanceof Error ? raised.message : String(raised)}`,
|
|
3431
|
+
"Check that the files this build writes can be written, then build again."
|
|
3432
|
+
);
|
|
3433
|
+
}
|
|
3434
|
+
}
|
|
3435
|
+
|
|
3436
|
+
// src/hexagon/application/CharterAuthoring.ts
|
|
3437
|
+
var CharterAuthoring = class {
|
|
3438
|
+
#repoPath;
|
|
3439
|
+
#fileReader;
|
|
3440
|
+
#yamlParser;
|
|
3441
|
+
#fileWriter;
|
|
3442
|
+
#vcs;
|
|
3443
|
+
constructor(repoPath2, fileReader2, yamlParser2, fileWriter2, vcs2) {
|
|
3444
|
+
this.#repoPath = repoPath2;
|
|
3445
|
+
this.#fileReader = fileReader2;
|
|
3446
|
+
this.#yamlParser = yamlParser2;
|
|
3447
|
+
this.#fileWriter = fileWriter2;
|
|
3448
|
+
this.#vcs = vcs2;
|
|
3449
|
+
}
|
|
3450
|
+
/**
|
|
3451
|
+
* The four questions about this repository, asked in one go (FR-040): which
|
|
3452
|
+
* agents it compiles for, what validating its charter finds, which installed
|
|
3453
|
+
* charters are edited here, and how many of those are unwell.
|
|
3454
|
+
*
|
|
3455
|
+
* Each is asked of whoever already answers it — the settings, the charter's
|
|
3456
|
+
* validation, version control — and whether each is well is worked out here,
|
|
3457
|
+
* so what `cw doctor` says and what the portal's health check says are one
|
|
3458
|
+
* answer said twice (SC-003). Reads and says: nothing here writes (FR-041).
|
|
3459
|
+
*
|
|
3460
|
+
* Where the charter holds, what a build would still change is the preview run
|
|
3461
|
+
* for it, and every guide and sensor no test case names is a warning too, so
|
|
3462
|
+
* the test files are read here: a file that will not read names no case, and
|
|
3463
|
+
* `cw test` is what refuses it (FR-014). Nothing else reads them for this — a
|
|
3464
|
+
* warning stops nothing, and every other use case asks only for the errors.
|
|
3465
|
+
* Where it does not hold, nothing is previewed (FR-005, FR-009).
|
|
3466
|
+
*/
|
|
3467
|
+
async doctor() {
|
|
3468
|
+
const [settings, [charter, , faultsByFiles], drifted, testRoot] = await Promise.all([
|
|
3469
|
+
loadSettings(this.#repoPath, this.#fileReader),
|
|
3470
|
+
this.#read(),
|
|
3471
|
+
driftedVendors(this.#repoPath, this.#vcs),
|
|
3472
|
+
loadTestRoot(this.#repoPath, this.#fileReader)
|
|
3473
|
+
]);
|
|
3474
|
+
if (charter === void 0) return doctorOutcomeDTO(settings, faultsByFiles, void 0, drifted, this.#repoPath);
|
|
3475
|
+
const { cleanupPlan, projectionPlan } = await plan(this.#repoPath, charter, settings.agents, this.#fileReader);
|
|
3476
|
+
return doctorOutcomeDTO(
|
|
3477
|
+
settings,
|
|
3478
|
+
faultsByFiles.with(findUntestedPrimitives(charter, Object.values(testRoot.suitesByFile))),
|
|
3479
|
+
previewPlan(cleanupPlan, projectionPlan),
|
|
3480
|
+
drifted,
|
|
3481
|
+
this.#repoPath
|
|
3482
|
+
);
|
|
3483
|
+
}
|
|
3484
|
+
/** Everything wrong with this repository's charter under the file that has to
|
|
3485
|
+
* change, and — where none of it is an error — the charter and what the
|
|
3486
|
+
* repository configured itself with (FR-005, FR-009): what every use case
|
|
3487
|
+
* here reads before it asks the charter anything. Settings that do not read
|
|
3488
|
+
* are neither: there is nothing to validate against until they do, so
|
|
3489
|
+
* reading them raises rather than reports. */
|
|
3490
|
+
async #read() {
|
|
3491
|
+
const [charter, settings] = await Promise.all([
|
|
3492
|
+
loadCharterRoot(this.#repoPath, this.#fileReader, this.#yamlParser),
|
|
3493
|
+
loadSettings(this.#repoPath, this.#fileReader)
|
|
3494
|
+
]);
|
|
3495
|
+
const faultsByFiles = charter.allFaultsByFiles;
|
|
3496
|
+
return faultsByFiles.errors().isEmpty ? [charter, settings, faultsByFiles] : [void 0, void 0, faultsByFiles];
|
|
3497
|
+
}
|
|
3498
|
+
/**
|
|
3499
|
+
* What the charter holds, listed, narrowed to one kind where one is named
|
|
3500
|
+
* (FR-011, FR-012).
|
|
3501
|
+
*
|
|
3502
|
+
* The listing is the one the charter compiles to, asked for the way everything
|
|
3503
|
+
* else is: `compile` hands back everything one reading produces, and this
|
|
3504
|
+
* takes the listing out of it (SC-004). No agent is named, since what an agent
|
|
3505
|
+
* reads is not what is being asked for here — a repository that compiles for
|
|
3506
|
+
* none still lists what it holds.
|
|
3507
|
+
*
|
|
3508
|
+
* Whether the word narrowing it is a kind at all is asked here, of the
|
|
3509
|
+
* domain that holds the set. A caller passes on what was typed and knows
|
|
3510
|
+
* nothing about what a kind is.
|
|
3511
|
+
*
|
|
3512
|
+
* Reads and says: this holds a writing port and never reaches for it, which is
|
|
3513
|
+
* what keeps a reading use case unable to write (FR-041).
|
|
3514
|
+
*/
|
|
3515
|
+
async list(kind) {
|
|
3516
|
+
if (kind !== void 0 && !isKind(kind))
|
|
3517
|
+
throw new Fault(`"${kind}" is not a kind the charter knows.`, `Ask for one the charter knows: ${KINDS.join(", ")}.`);
|
|
3518
|
+
const [charter, , faultsByFiles] = await this.#read();
|
|
3519
|
+
if (charter === void 0) return faultsByFileDTO(faultsByFiles.errors(), this.#repoPath);
|
|
3520
|
+
const { catalogue: catalogue2 } = compile(charter, []);
|
|
3521
|
+
return catalogueDTO(kind === void 0 ? catalogue2 : catalogue2.filterByKind(kind));
|
|
3522
|
+
}
|
|
3523
|
+
/** Every primitive the charter read, or those mentioning a word, each as the
|
|
3524
|
+
* whole charter sees it (FR-112, FR-114). Whether one mentions the word is
|
|
3525
|
+
* the primitive's own to say. */
|
|
3526
|
+
async fullList(matching) {
|
|
3527
|
+
const [charter, , faultsByFiles] = await this.#read();
|
|
3528
|
+
if (charter === void 0) return faultsByFileDTO(faultsByFiles.errors(), this.#repoPath);
|
|
3529
|
+
return scopedPrimitivesDTO(
|
|
3530
|
+
matching === void 0 ? charter.primitives : charter.primitives.filter((one) => one.mentions(matching))
|
|
3531
|
+
);
|
|
3532
|
+
}
|
|
3533
|
+
/**
|
|
3534
|
+
* Which file declares one identity, which layer it arrived in, when it comes
|
|
3535
|
+
* up, the mixins it uses and the corpus it cites, what uses or cites it, and
|
|
3536
|
+
* the situations this repository wrote down about it (FR-014, FR-017).
|
|
3537
|
+
*
|
|
3538
|
+
* The charter is asked, not searched here: one identity space covers every
|
|
3539
|
+
* layer, and which file answers to a name, and which names one primitive
|
|
3540
|
+
* shares with another, are the charter's own questions (FR-014). What is here
|
|
3541
|
+
* is the order — read, refuse a charter that does not hold, then ask.
|
|
3542
|
+
*
|
|
3543
|
+
* An identity nothing answers to is raised rather than reported, the way a
|
|
3544
|
+
* word that is no kind is: there is no file it is wrong with, and the next
|
|
3545
|
+
* move is to go and look at what the charter does hold.
|
|
3546
|
+
*
|
|
3547
|
+
* Reads and says: this holds a writing port and never reaches for it (FR-041).
|
|
3548
|
+
*/
|
|
3549
|
+
async explain(identity) {
|
|
3550
|
+
const primitiveIdentity = identityOf(identity);
|
|
3551
|
+
const [charter, , faultsByFiles] = await this.#read();
|
|
3552
|
+
if (charter === void 0) return faultsByFileDTO(faultsByFiles.errors(), this.#repoPath);
|
|
3553
|
+
const declared = charter.primitiveById.get(primitiveIdentity);
|
|
3554
|
+
if (declared === void 0)
|
|
3555
|
+
throw new Fault(
|
|
3556
|
+
`This charter holds no "${identity}".`,
|
|
3557
|
+
'Run "cw list --min" to see every identity it does hold.'
|
|
3558
|
+
);
|
|
3559
|
+
const testRoot = await loadTestRoot(this.#repoPath, this.#fileReader);
|
|
3560
|
+
const testCasesByFile2 = Object.fromEntries(
|
|
3561
|
+
Object.entries(testRoot.suitesByFile).map(([file, suite]) => [
|
|
3562
|
+
file,
|
|
3563
|
+
suite.cases.filter((each) => each.activatedIdentity === primitiveIdentity).map((each) => each.describe())
|
|
3564
|
+
]).filter(([, situations]) => situations.length > 0)
|
|
3565
|
+
);
|
|
3566
|
+
return explanationOutcomeDTO(
|
|
3567
|
+
declared,
|
|
3568
|
+
// When it comes up is its kind's to say, in the words `cw kinds` says it
|
|
3569
|
+
// in, so it is read off the class that reads the kind (FR-029).
|
|
3570
|
+
PRIMITIVE_CLASSES.find((one) => one.kind === declared.primitive.kind).activatesWhen,
|
|
3571
|
+
charter.mixinsOf(declared),
|
|
3572
|
+
charter.rationaleOf(declared),
|
|
3573
|
+
charter.hostsOf(declared),
|
|
3574
|
+
charter.citersOf(declared),
|
|
3575
|
+
testCasesByFile2
|
|
3576
|
+
);
|
|
3577
|
+
}
|
|
3578
|
+
/**
|
|
3579
|
+
* One reading of the charter, put on disk (FR-020, FR-021).
|
|
3580
|
+
*
|
|
3581
|
+
* Read, ask what is wrong, take away what the last build left, then compile and
|
|
3582
|
+
* write. The order is the whole of this method: a charter with an error is
|
|
3583
|
+
* refused before anything is touched, so a half-built surface never stands —
|
|
3584
|
+
* and a warning is not an error, so a rationale citing a corpus that is gone
|
|
3585
|
+
* still builds (FR-005, FR-009).
|
|
3586
|
+
*
|
|
3587
|
+
* Which agents are compiled for is what the repository said at setup and
|
|
3588
|
+
* keeps in `.cw/settings.json`, never what happens to be installed on the
|
|
3589
|
+
* machine running this: the same charter checked out anywhere builds the same
|
|
3590
|
+
* files (FR-033). A repository naming none still gets its neutral surface
|
|
3591
|
+
* (FR-019), and one whose settings do not read is sent back to `cw init`
|
|
3592
|
+
* rather than built against a guess.
|
|
3593
|
+
*
|
|
3594
|
+
* What a file holds and where it goes is `compile` and the two plans; what is
|
|
3595
|
+
* here is the order of the three and the ports that make it possible.
|
|
3596
|
+
*/
|
|
3597
|
+
async build() {
|
|
3598
|
+
const [charter, settings, faultsByFiles] = await this.#read();
|
|
3599
|
+
if (charter === void 0) return faultsByFileDTO(faultsByFiles.errors(), this.#repoPath);
|
|
3600
|
+
const { cleanupPlan, projectionPlan } = await plan(this.#repoPath, charter, settings.agents, this.#fileReader);
|
|
3601
|
+
const planSummary2 = previewPlan(cleanupPlan, projectionPlan);
|
|
3602
|
+
await executePlan(cleanupPlan, projectionPlan, this.#fileWriter);
|
|
3603
|
+
return planSummaryDTO(planSummary2);
|
|
3604
|
+
}
|
|
3605
|
+
/**
|
|
3606
|
+
* The build, worked out and stopped short of disk (FR-022).
|
|
3607
|
+
*
|
|
3608
|
+
* The same two plans a build carries out, read rather than run: planning only
|
|
3609
|
+
* reads, and what a preview adds is `previewPlan` in place of `executePlan`.
|
|
3610
|
+
* It holds a writing port, as everything here does, and reaches for none of it
|
|
3611
|
+
* (FR-041).
|
|
3612
|
+
*/
|
|
3613
|
+
async preview() {
|
|
3614
|
+
const [charter, settings, faultsByFiles] = await this.#read();
|
|
3615
|
+
if (charter === void 0) return faultsByFileDTO(faultsByFiles.errors(), this.#repoPath);
|
|
3616
|
+
const { cleanupPlan, projectionPlan } = await plan(this.#repoPath, charter, settings.agents, this.#fileReader);
|
|
3617
|
+
return planSummaryDTO(previewPlan(cleanupPlan, projectionPlan));
|
|
3618
|
+
}
|
|
3619
|
+
/**
|
|
3620
|
+
* Every situation this repository wrote down, resolved against its charter
|
|
3621
|
+
* (FR-048, FR-049, FR-050).
|
|
3622
|
+
*
|
|
3623
|
+
* The same two steps every use case here is: read the charter, then ask it.
|
|
3624
|
+
* What is added is the cases, read off `.cw/test/` by whoever knows where they
|
|
3625
|
+
* live, and the resolving each one comes out of — which runs nothing, fetches
|
|
3626
|
+
* nothing and asks no agent, so a run is decided by the charter alone.
|
|
3627
|
+
*
|
|
3628
|
+
* A charter with an error resolves nothing, for the reason a listing lists
|
|
3629
|
+
* nothing, and neither does a test file that is there and will not read — both
|
|
3630
|
+
* come back as faults under the file that has to change (FR-009). A repository
|
|
3631
|
+
* that wrote no tests resolves nothing either, and that is no fault: it has
|
|
3632
|
+
* not pinned its charter down yet, and the command says so.
|
|
3633
|
+
*
|
|
3634
|
+
* Reads and says: this holds a writing port and never reaches for it (FR-041).
|
|
3635
|
+
*/
|
|
3636
|
+
async test() {
|
|
3637
|
+
const [charter, , faultsByFiles] = await this.#read();
|
|
3638
|
+
if (charter === void 0) return faultsByFileDTO(faultsByFiles.errors(), this.#repoPath);
|
|
3639
|
+
const testRoot = await loadTestRoot(this.#repoPath, this.#fileReader);
|
|
3640
|
+
if (!testRoot.faultsByFiles.isEmpty) return faultsByFileDTO(testRoot.faultsByFiles, this.#repoPath);
|
|
3641
|
+
return testRunReportDTO(
|
|
3642
|
+
new TestRunReport(
|
|
3643
|
+
Object.entries(testRoot.suitesByFile).flatMap(([file, suite]) => runSuite(charter, testSuiteNameOf(file), suite))
|
|
3644
|
+
)
|
|
3645
|
+
);
|
|
3646
|
+
}
|
|
3647
|
+
/** What one kind takes of whoever authors a primitive of it, asked of the
|
|
3648
|
+
* kind itself (FR-004, FR-039). No charter is read: a kind's contract is the
|
|
3649
|
+
* same in a repository that has authored nothing. A word that is no kind is
|
|
3650
|
+
* raised the way a listing raises it — there is no file it is wrong with. */
|
|
3651
|
+
async listPrimitiveRequirements(kind) {
|
|
3652
|
+
const primitiveKind = this.#kindOf(kind);
|
|
3653
|
+
return primitiveRequirementsDTO(
|
|
3654
|
+
new PrimitiveRequirements(primitiveHeadersOf(primitiveKind), primitiveSampleOf(primitiveKind))
|
|
3655
|
+
);
|
|
3656
|
+
}
|
|
3657
|
+
/** Every kind the charter knows, and when a primitive of that kind comes up
|
|
3658
|
+
* (FR-010). Asked of the classes themselves, as the requirements above are:
|
|
3659
|
+
* no charter is read, and a kind this repository has authored nothing of is a
|
|
3660
|
+
* kind all the same. */
|
|
3661
|
+
async kinds() {
|
|
3662
|
+
return primitiveKindsDTO(PRIMITIVE_CLASSES);
|
|
3663
|
+
}
|
|
3664
|
+
/**
|
|
3665
|
+
* One primitive written where its kind is authored (FR-039), with the body
|
|
3666
|
+
* its author typed — empty from the command line, where the body is written
|
|
3667
|
+
* in an editor afterwards (FR-117).
|
|
3668
|
+
*
|
|
3669
|
+
* Nothing is validated and nothing is asked of the charter but which
|
|
3670
|
+
* identities it already holds. What arrives is what an author answered, and
|
|
3671
|
+
* reading it as the primitive it claims to be is what refuses an answer its
|
|
3672
|
+
* kind will not take — the same reading a file of it would get, so there is no
|
|
3673
|
+
* second contract here to keep in step with the kind's (FR-004). Where it goes
|
|
3674
|
+
* is the convention: a directory per kind, since nothing reads the directory
|
|
3675
|
+
* (FR-002).
|
|
3676
|
+
*
|
|
3677
|
+
* An identity the charter already holds, in any layer, is refused naming the
|
|
3678
|
+
* file holding it: one identity names one primitive in the whole charter, and
|
|
3679
|
+
* a second file claiming it is a collision the next read would report
|
|
3680
|
+
* (FR-014). The files are read without being validated, so a charter wrong
|
|
3681
|
+
* somewhere else can still be added to.
|
|
3682
|
+
*/
|
|
3683
|
+
async add(kindWord, id, headers, body = "") {
|
|
3684
|
+
const kind = this.#kindOf(kindWord);
|
|
3685
|
+
let primitive;
|
|
3686
|
+
try {
|
|
3687
|
+
primitive = primitiveOf({ headers: { ...headers, kind, id }, body });
|
|
3688
|
+
} catch (raised) {
|
|
3689
|
+
if (!(raised instanceof AggregateError)) throw raised;
|
|
3690
|
+
return faultsDTO(new Faults(raised.errors));
|
|
3691
|
+
}
|
|
3692
|
+
const charter = await loadCharterRoot(this.#repoPath, this.#fileReader, this.#yamlParser);
|
|
3693
|
+
const claimingPrimitive = charter.primitiveById.get(identityOf({ kind, id }));
|
|
3694
|
+
if (claimingPrimitive !== void 0)
|
|
3695
|
+
throw new Fault(
|
|
3696
|
+
`"${identityOf({ kind, id })}" is already there, declared by ${claimingPrimitive.file}, and one identity names one primitive in the whole charter.`,
|
|
3697
|
+
`Open ${claimingPrimitive.file}, or run this again with an id this charter has not got.`
|
|
3698
|
+
);
|
|
3699
|
+
return scopedPrimitiveDTO(await writeCharter(this.#repoPath, primitive, this.#fileReader, this.#fileWriter));
|
|
3700
|
+
}
|
|
3701
|
+
/**
|
|
3702
|
+
* One primitive as the charter read it: its headers, its body, the file and
|
|
3703
|
+
* layer it is in, and its revision (FR-075, FR-078).
|
|
3704
|
+
*
|
|
3705
|
+
* Asked of the files as read rather than of the validation, so a primitive
|
|
3706
|
+
* opens whatever else in the charter is wrong — which is when its author most
|
|
3707
|
+
* needs to open it. The revision is the content hash of `toMarkdown()`, the
|
|
3708
|
+
* text a save writes, so a save made over it is checked against the primitive
|
|
3709
|
+
* read again then.
|
|
3710
|
+
*/
|
|
3711
|
+
async open(identity) {
|
|
3712
|
+
const scopedPrimitive2 = await this.#scopedPrimitiveOf(identity);
|
|
3713
|
+
return primitiveSnapshotDTO(scopedPrimitive2, await contentHashOf(scopedPrimitive2.primitive.toMarkdown()));
|
|
3714
|
+
}
|
|
3715
|
+
/**
|
|
3716
|
+
* One repository primitive's file written over with new headers and a new
|
|
3717
|
+
* body, keeping its kind, its id and the file it is in (FR-075).
|
|
3718
|
+
*
|
|
3719
|
+
* Refused — raised, with nothing written — for a primitive this repository
|
|
3720
|
+
* did not author (FR-077), and for one whose revision is no longer the one
|
|
3721
|
+
* its author opened, so an edit made on disk since is not lost under
|
|
3722
|
+
* this one (FR-078). Answers the kind will not take come back as the faults
|
|
3723
|
+
* `add` gives for them, since both read answers the one way.
|
|
3724
|
+
*
|
|
3725
|
+
* Kind and id are the identity's, never the headers': an edit cannot move a
|
|
3726
|
+
* file, and a rename is a new primitive and a deletion. Nothing is compiled
|
|
3727
|
+
* and nothing committed (FR-079).
|
|
3728
|
+
*/
|
|
3729
|
+
async rewrite(identity, headers, body, revision) {
|
|
3730
|
+
const scopedPrimitive2 = await this.#scopedPrimitiveOf(identity);
|
|
3731
|
+
if (!RepoScopedPrimitive.isSatisfiedBy(scopedPrimitive2))
|
|
3732
|
+
throw new Fault(
|
|
3733
|
+
`${identity} was not authored in this repository, and ${scopedPrimitive2.file} is read-only here.`,
|
|
3734
|
+
`To differ from it, author a primitive of your own under an identity of its own.`
|
|
3735
|
+
);
|
|
3736
|
+
if (await contentHashOf(scopedPrimitive2.primitive.toMarkdown()) !== revision)
|
|
3737
|
+
throw new Fault(
|
|
3738
|
+
`${scopedPrimitive2.file} changed on disk after it was opened, and saving would write over that change.`,
|
|
3739
|
+
`Open it again to see what changed, then make your edit there.`
|
|
3740
|
+
);
|
|
3741
|
+
let primitive;
|
|
3742
|
+
try {
|
|
3743
|
+
primitive = primitiveOf({
|
|
3744
|
+
headers: { ...headers, kind: scopedPrimitive2.primitive.kind, id: scopedPrimitive2.primitive.headers.id },
|
|
3745
|
+
body
|
|
3746
|
+
});
|
|
3747
|
+
} catch (raised) {
|
|
3748
|
+
if (!(raised instanceof AggregateError)) throw raised;
|
|
3749
|
+
return faultsDTO(new Faults(raised.errors));
|
|
3750
|
+
}
|
|
3751
|
+
await this.#fileWriter.write(new URL(scopedPrimitive2.file, this.#repoPath), primitive.toMarkdown());
|
|
3752
|
+
return scopedPrimitiveDTO(new ScopedPrimitive(identity, REPO_SCOPE, scopedPrimitive2.file, primitive));
|
|
3753
|
+
}
|
|
3754
|
+
/**
|
|
3755
|
+
* One repository primitive's file taken away, and nothing else (FR-076).
|
|
3756
|
+
*
|
|
3757
|
+
* Refused for a primitive this repository did not author, as a rewrite is
|
|
3758
|
+
* (FR-077). What still names it is left as it is: the next validation reports
|
|
3759
|
+
* each as dangling, and what to do about them is its author's call. Nothing is
|
|
3760
|
+
* compiled and nothing committed (FR-079).
|
|
3761
|
+
*/
|
|
3762
|
+
async remove(identity) {
|
|
3763
|
+
const scopedPrimitive2 = await this.#scopedPrimitiveOf(identity);
|
|
3764
|
+
if (!RepoScopedPrimitive.isSatisfiedBy(scopedPrimitive2))
|
|
3765
|
+
throw new Fault(
|
|
3766
|
+
`${identity} was not authored in this repository, and ${scopedPrimitive2.file} is read-only here.`,
|
|
3767
|
+
`To differ from it, author a primitive of your own under an identity of its own.`
|
|
3768
|
+
);
|
|
3769
|
+
await this.#fileWriter.delete(new URL(scopedPrimitive2.file, this.#repoPath));
|
|
3770
|
+
return scopedPrimitiveDTO(scopedPrimitive2);
|
|
3771
|
+
}
|
|
3772
|
+
/** The primitive one identity names, off the files as read and not the
|
|
3773
|
+
* validation. Raised when the charter holds nothing of it, the way `explain`
|
|
3774
|
+
* raises it: there is no file it is wrong with. */
|
|
3775
|
+
async #scopedPrimitiveOf(identity) {
|
|
3776
|
+
const primitiveIdentity = identityOf(identity);
|
|
3777
|
+
const charter = await loadCharterRoot(this.#repoPath, this.#fileReader, this.#yamlParser);
|
|
3778
|
+
const scopedPrimitive2 = charter.primitiveById.get(primitiveIdentity);
|
|
3779
|
+
if (scopedPrimitive2 === void 0)
|
|
3780
|
+
throw new Fault(
|
|
3781
|
+
`This charter holds no "${identity}".`,
|
|
3782
|
+
'Run "cw list --min" to see every identity it does hold.'
|
|
3783
|
+
);
|
|
3784
|
+
return scopedPrimitive2;
|
|
3785
|
+
}
|
|
3786
|
+
/** One word read as the kind it names. Raised rather than reported, the way a
|
|
3787
|
+
* listing's is: there is no file it is wrong with, and what comes back names
|
|
3788
|
+
* every kind there is. */
|
|
3789
|
+
#kindOf(kind) {
|
|
3790
|
+
if (!isKind(kind))
|
|
3791
|
+
throw new Fault(`"${kind}" is not a kind the charter knows.`, `Ask for one the charter knows: ${KINDS.join(", ")}.`);
|
|
3792
|
+
return kind;
|
|
3793
|
+
}
|
|
3794
|
+
/** What this repository configured itself with, read the one way settings are
|
|
3795
|
+
* ever read (FR-038). */
|
|
3796
|
+
async settings() {
|
|
3797
|
+
return workspaceSettingsDTO(await loadSettings(this.#repoPath, this.#fileReader));
|
|
3798
|
+
}
|
|
3799
|
+
/**
|
|
3800
|
+
* Refused outside version control, and then without a settings file, which
|
|
3801
|
+
* setup writes whatever it was answered: its absence is what a repository
|
|
3802
|
+
* never set up looks like, while `loadSettings` reads it as settings naming
|
|
3803
|
+
* no agent (FR-004, FR-037).
|
|
3804
|
+
*/
|
|
3805
|
+
async ensureRepoReady() {
|
|
3806
|
+
if (!await this.#vcs.isInstalled(this.#repoPath))
|
|
3807
|
+
throw new Fault(
|
|
3808
|
+
"This folder is not inside a git repository, and a charter is authored inside one.",
|
|
3809
|
+
'Run "git init" here, then "cw init", or run this again where your repository is.'
|
|
3810
|
+
);
|
|
3811
|
+
if (await this.#fileReader.readIfThere(settingsFileIn(this.#repoPath)) === void 0)
|
|
3812
|
+
throw new Fault("This repository was never set up under a charter.", 'Run "cw init" first.');
|
|
3813
|
+
}
|
|
3814
|
+
/**
|
|
3815
|
+
* One repository, set up under a charter (FR-032, FR-037).
|
|
3816
|
+
*
|
|
3817
|
+
* Refused outside version control before anything is written: what setup
|
|
3818
|
+
* writes is committed as one change, and a folder with nothing to commit into
|
|
3819
|
+
* is a folder this has no business writing in (FR-032).
|
|
3820
|
+
*
|
|
3821
|
+
* Refused, too, without an agent to compile for: a charter is authored to
|
|
3822
|
+
* instruct one, and a repository compiling for none would build a surface no
|
|
3823
|
+
* agent it runs reads (FR-033).
|
|
3824
|
+
*
|
|
3825
|
+
* Then the two things setup owns — what the repository configured itself
|
|
3826
|
+
* with, and a directory per kind for a charter to be authored in — and
|
|
3827
|
+
* nothing more: compiling is `build`'s, run once there is a charter to
|
|
3828
|
+
* compile (FR-037). What comes back is that it is set up; what stops it is
|
|
3829
|
+
* raised.
|
|
3830
|
+
*
|
|
3831
|
+
* Nothing is written into an ignore file: everything under the workspace is
|
|
3832
|
+
* committed, what a vendor installed included, so a checkout of this
|
|
3833
|
+
* repository holds the whole charter it is governed by without fetching
|
|
3834
|
+
* anything.
|
|
3835
|
+
*
|
|
3836
|
+
* Every one of those is written whole and none of them is authored content,
|
|
3837
|
+
* so running this again reconfigures the repository and discards nothing
|
|
3838
|
+
* anybody wrote (FR-038).
|
|
3839
|
+
*/
|
|
3840
|
+
async init({ agents }) {
|
|
3841
|
+
if (!await this.#vcs.isInstalled(this.#repoPath))
|
|
3842
|
+
throw new Fault(
|
|
3843
|
+
"This folder is not inside a git repository, and a charter is authored inside one.",
|
|
3844
|
+
'Run "git init" here, or run this again where your repository is.'
|
|
3845
|
+
);
|
|
3846
|
+
if (agents.length === 0)
|
|
3847
|
+
throw new Fault(
|
|
3848
|
+
"Setup was given no agent to compile for, and a charter compiles for at least one.",
|
|
3849
|
+
`Choose at least one of: ${AGENT_PROVIDERS.join(", ")}.`
|
|
3850
|
+
);
|
|
3851
|
+
await this.#fileWriter.write(settingsFileIn(this.#repoPath), `${JSON.stringify({ agents }, null, 2)}
|
|
3852
|
+
`);
|
|
3853
|
+
const kindFolders = KINDS.map((kind) => `${CHARTER_DIRECTORY}/${kind}/.gitkeep`);
|
|
3854
|
+
for (const path of kindFolders) await this.#fileWriter.write(new URL(path, this.#repoPath), "");
|
|
3855
|
+
return true;
|
|
3856
|
+
}
|
|
3857
|
+
};
|
|
3858
|
+
|
|
3859
|
+
// src/hexagon/application/CharterVendoring.ts
|
|
3860
|
+
var CharterVendoring = class {
|
|
3861
|
+
#repoPath;
|
|
3862
|
+
#fileReader;
|
|
3863
|
+
#vcs;
|
|
3864
|
+
constructor(repoPath2, fileReader2, vcs2) {
|
|
3865
|
+
this.#repoPath = repoPath2;
|
|
3866
|
+
this.#fileReader = fileReader2;
|
|
3867
|
+
this.#vcs = vcs2;
|
|
3868
|
+
}
|
|
3869
|
+
async add(source, version) {
|
|
3870
|
+
return addVendor(this.#repoPath, this.#vcs, source, version);
|
|
3871
|
+
}
|
|
3872
|
+
async remove(name) {
|
|
3873
|
+
return removeVendor(this.#repoPath, this.#vcs, name);
|
|
3874
|
+
}
|
|
3875
|
+
async installed() {
|
|
3876
|
+
return loadVendorNames(this.#repoPath, this.#fileReader);
|
|
3877
|
+
}
|
|
3878
|
+
};
|
|
3879
|
+
|
|
3880
|
+
// src/hexagon/application/TestAuthoring.ts
|
|
3881
|
+
var TestAuthoring = class {
|
|
3882
|
+
#repoPath;
|
|
3883
|
+
#fileReader;
|
|
3884
|
+
#fileWriter;
|
|
3885
|
+
constructor(repoPath2, fileReader2, fileWriter2) {
|
|
3886
|
+
this.#repoPath = repoPath2;
|
|
3887
|
+
this.#fileReader = fileReader2;
|
|
3888
|
+
this.#fileWriter = fileWriter2;
|
|
3889
|
+
}
|
|
3890
|
+
/** Every test file as it was read, with its text, one that does not read
|
|
3891
|
+
* among them (FR-124). */
|
|
3892
|
+
async suites() {
|
|
3893
|
+
return testSuitesDTO(await loadTestRoot(this.#repoPath, this.#fileReader));
|
|
3894
|
+
}
|
|
3895
|
+
async addSuite() {
|
|
3896
|
+
return addTestSuite(this.#repoPath, this.#fileReader, this.#fileWriter);
|
|
3897
|
+
}
|
|
3898
|
+
async writeSuite(name, text) {
|
|
3899
|
+
return writeTestSuite(this.#repoPath, this.#fileReader, this.#fileWriter, name, text);
|
|
3900
|
+
}
|
|
3901
|
+
async removeSuite(name) {
|
|
3902
|
+
return removeTestSuite(this.#repoPath, this.#fileReader, this.#fileWriter, name);
|
|
3903
|
+
}
|
|
3904
|
+
};
|
|
3905
|
+
|
|
3906
|
+
// src/zdriven/FileReaders.ts
|
|
3907
|
+
import { readdir, readFile } from "fs/promises";
|
|
3908
|
+
var FileReaders = class {
|
|
3909
|
+
async readFilesRecursively(folder) {
|
|
3910
|
+
const found = await Promise.all(
|
|
3911
|
+
(await entriesOf(folder)).map(async (entry) => {
|
|
3912
|
+
if (entry.isDirectory()) return this.readFilesRecursively(under(folder, entry.name));
|
|
3913
|
+
const file = new URL(encodeURIComponent(entry.name), folder);
|
|
3914
|
+
return [{ file, contents: await readFile(file, "utf8") }];
|
|
3915
|
+
})
|
|
3916
|
+
);
|
|
3917
|
+
return found.flat();
|
|
3918
|
+
}
|
|
3919
|
+
async listFolders(folder) {
|
|
3920
|
+
return (await entriesOf(folder)).filter((entry) => entry.isDirectory()).map((entry) => under(folder, entry.name));
|
|
3921
|
+
}
|
|
3922
|
+
async readIfThere(file) {
|
|
3923
|
+
try {
|
|
3924
|
+
return await readFile(file, "utf8");
|
|
3925
|
+
} catch (raised) {
|
|
3926
|
+
if (raised.code === "ENOENT") return void 0;
|
|
3927
|
+
throw raised;
|
|
3928
|
+
}
|
|
3929
|
+
}
|
|
3930
|
+
};
|
|
3931
|
+
async function entriesOf(at) {
|
|
3932
|
+
try {
|
|
3933
|
+
return await readdir(at, { withFileTypes: true });
|
|
3934
|
+
} catch (raised) {
|
|
3935
|
+
if (raised.code === "ENOENT") return [];
|
|
3936
|
+
throw raised;
|
|
3937
|
+
}
|
|
3938
|
+
}
|
|
3939
|
+
function under(at, directory) {
|
|
3940
|
+
return new URL(`${encodeURIComponent(directory)}/`, at);
|
|
3941
|
+
}
|
|
3942
|
+
|
|
3943
|
+
// src/zdriven/YamlParser.ts
|
|
3944
|
+
import { parse } from "yaml";
|
|
3945
|
+
var YamlParser = class {
|
|
3946
|
+
parse(block) {
|
|
3947
|
+
let value;
|
|
3948
|
+
try {
|
|
3949
|
+
value = parse(block, { merge: false });
|
|
3950
|
+
} catch (cause) {
|
|
3951
|
+
throw new CharterPrimitiveFault(
|
|
3952
|
+
`The frontmatter is not readable: ${cause.message.split("\n")[0]}`,
|
|
3953
|
+
`Correct the notation: each header is "name: value", one per line.`
|
|
3954
|
+
);
|
|
3955
|
+
}
|
|
3956
|
+
if (value === null || value === void 0) return {};
|
|
3957
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
3958
|
+
throw new CharterPrimitiveFault(
|
|
3959
|
+
"The frontmatter must be a list of named headers, one per line \u2014 not a bare value or a list.",
|
|
3960
|
+
`Write it as "id: <name>" and the rest of the headers this kind requires, one per line.`
|
|
3961
|
+
);
|
|
3962
|
+
}
|
|
3963
|
+
return value;
|
|
3964
|
+
}
|
|
3965
|
+
};
|
|
3966
|
+
|
|
3967
|
+
// src/zdriven/FileOutput.ts
|
|
3968
|
+
import { mkdir, rm, writeFile } from "fs/promises";
|
|
3969
|
+
var FileOutput = class {
|
|
3970
|
+
async write(file, contents) {
|
|
3971
|
+
await mkdir(new URL(".", file), { recursive: true });
|
|
3972
|
+
await writeFile(file, contents, "utf8");
|
|
3973
|
+
}
|
|
3974
|
+
async delete(file) {
|
|
3975
|
+
await rm(file, { force: true });
|
|
3976
|
+
}
|
|
3977
|
+
};
|
|
3978
|
+
|
|
3979
|
+
// src/zdriven/Git.ts
|
|
3980
|
+
import { execFile } from "child_process";
|
|
3981
|
+
import { promisify } from "util";
|
|
3982
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3983
|
+
var run = promisify(execFile);
|
|
3984
|
+
var Git = class {
|
|
3985
|
+
/** How it looks at a folder. Reading files is somebody else's port, and this
|
|
3986
|
+
* asks it rather than reaching for the filesystem itself. */
|
|
3987
|
+
constructor(fileReader2) {
|
|
3988
|
+
this.fileReader = fileReader2;
|
|
3989
|
+
}
|
|
3990
|
+
fileReader;
|
|
3991
|
+
async isInstalled(folder) {
|
|
3992
|
+
try {
|
|
3993
|
+
await run("git", ["rev-parse", "--git-dir"], { cwd: fileURLToPath2(folder) });
|
|
3994
|
+
return true;
|
|
3995
|
+
} catch {
|
|
3996
|
+
return false;
|
|
3997
|
+
}
|
|
3998
|
+
}
|
|
3999
|
+
/** `git status --porcelain`: nothing said is nothing staged and nothing
|
|
4000
|
+
* modified. A folder git refuses to answer about is not clean either — there
|
|
4001
|
+
* is no repository there to have anything in hand. */
|
|
4002
|
+
async isClean(folder) {
|
|
4003
|
+
try {
|
|
4004
|
+
const { stdout } = await run("git", ["status", "--porcelain"], { cwd: fileURLToPath2(folder) });
|
|
4005
|
+
return stdout.trim() === "";
|
|
4006
|
+
} catch {
|
|
4007
|
+
return false;
|
|
4008
|
+
}
|
|
4009
|
+
}
|
|
4010
|
+
/**
|
|
4011
|
+
* `git status --porcelain` narrowed to one folder, with every untracked file
|
|
4012
|
+
* named rather than the folder holding them: what a vendor folder holds that
|
|
4013
|
+
* the commit it arrived in does not (FR-040).
|
|
4014
|
+
*
|
|
4015
|
+
* The path comes off each line after the two status letters and the space
|
|
4016
|
+
* following them; a rename says `old -> new`, and what is there now is the
|
|
4017
|
+
* half that matters. A folder git refuses to answer about has nothing
|
|
4018
|
+
* changed under it, for the reason a folder outside version control is not
|
|
4019
|
+
* clean: there is nothing recorded to differ from.
|
|
4020
|
+
*/
|
|
4021
|
+
async changedUnder(repo, subFolder) {
|
|
4022
|
+
try {
|
|
4023
|
+
const { stdout } = await run("git", ["status", "--porcelain", "--untracked-files=all", "--", subFolder], {
|
|
4024
|
+
cwd: fileURLToPath2(repo)
|
|
4025
|
+
});
|
|
4026
|
+
return stdout.split("\n").filter((line) => line.trim() !== "").map((line) => line.slice(3).trim()).map((path) => path.split(" -> ").at(-1) ?? path);
|
|
4027
|
+
} catch {
|
|
4028
|
+
return [];
|
|
4029
|
+
}
|
|
4030
|
+
}
|
|
4031
|
+
/**
|
|
4032
|
+
* `git subtree`, handed the source exactly as it was written: `add` where the
|
|
4033
|
+
* folder is not there yet and `pull` where it is, so installing a source and
|
|
4034
|
+
* bringing it up to date are one call (FR-023, FR-030).
|
|
4035
|
+
*
|
|
4036
|
+
* Which transport it is, whether the credentials are there, whether the
|
|
4037
|
+
* version exists: git answers all three, and what it says is what the user
|
|
4038
|
+
* reads (FR-031).
|
|
4039
|
+
*
|
|
4040
|
+
* `--squash` because what is installed is content to read and not a history
|
|
4041
|
+
* to walk: one commit lands, holding the source's files under the folder
|
|
4042
|
+
* given (FR-028). `HEAD` where no version is named, which is what the source
|
|
4043
|
+
* calls its own default.
|
|
4044
|
+
*/
|
|
4045
|
+
async subtreeAdd(source, repo, intoSubFolder, version) {
|
|
4046
|
+
const cwd = fileURLToPath2(repo);
|
|
4047
|
+
const targetFolder = new URL(`${intoSubFolder}/`, repo);
|
|
4048
|
+
const targetFolderExist = (await this.fileReader.readFilesRecursively(targetFolder)).length > 0;
|
|
4049
|
+
try {
|
|
4050
|
+
await run(
|
|
4051
|
+
"git",
|
|
4052
|
+
["subtree", targetFolderExist ? "pull" : "add", `--prefix=${intoSubFolder}`, "--squash", "--", source, version ?? "HEAD"],
|
|
4053
|
+
{ cwd }
|
|
4054
|
+
);
|
|
4055
|
+
} catch (raised) {
|
|
4056
|
+
throw new Fault(
|
|
4057
|
+
`${raised}`,
|
|
4058
|
+
version === void 0 ? "Check the address, and that you can reach it from here." : `Check the address, and that "${version}" is a tag or branch it has.`
|
|
4059
|
+
);
|
|
4060
|
+
}
|
|
4061
|
+
}
|
|
4062
|
+
/** `git rm -r` and a commit of its own: the folder is gone from the working
|
|
4063
|
+
* tree and from the history from here on, the way adding it put it into
|
|
4064
|
+
* both. */
|
|
4065
|
+
async removeSubFolder(repo, subFolder) {
|
|
4066
|
+
const cwd = fileURLToPath2(repo);
|
|
4067
|
+
try {
|
|
4068
|
+
await run("git", ["rm", "-r", "--quiet", "--", subFolder], { cwd });
|
|
4069
|
+
await run("git", ["commit", "-qm", `Remove ${subFolder}`], { cwd });
|
|
4070
|
+
} catch (raised) {
|
|
4071
|
+
throw new Fault(`${raised}`, `Check that "${subFolder}" is a folder this repository holds.`);
|
|
4072
|
+
}
|
|
4073
|
+
}
|
|
4074
|
+
};
|
|
4075
|
+
|
|
4076
|
+
// main.ts
|
|
4077
|
+
var fileReader = new FileReaders();
|
|
4078
|
+
var yamlParser = new YamlParser();
|
|
4079
|
+
var fileWriter = new FileOutput();
|
|
4080
|
+
var vcs = new Git(fileReader);
|
|
4081
|
+
var repoPath = pathToFileURL2(`${process.cwd()}/`);
|
|
4082
|
+
var charterAuthoringApp = new CharterAuthoring(repoPath, fileReader, yamlParser, fileWriter, vcs);
|
|
4083
|
+
var charterVendoringApp = new CharterVendoring(repoPath, fileReader, vcs);
|
|
4084
|
+
var testAuthoringApp = new TestAuthoring(repoPath, fileReader, fileWriter);
|
|
4085
|
+
var cli = new Commander({ cwd: process.cwd(), version: package_default.version, charterAuthoringApp, charterVendoringApp, testAuthoringApp });
|
|
4086
|
+
process.exitCode = await cli.run(process.argv.slice(2));
|