@freetesh/zeno 0.12.0 → 0.13.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/.claude-plugin/plugin.json +1 -1
- package/README.md +11 -12
- package/cli/dist/checks/external.js +1 -0
- package/cli/dist/checks/refs.js +4 -2
- package/cli/dist/checks/shared-files.js +47 -59
- package/cli/dist/checks/sources.js +2 -2
- package/cli/dist/config/load.js +2 -3
- package/cli/dist/config/schema.js +13 -19
- package/cli/dist/format.js +1 -3
- package/cli/dist/init.js +1 -1
- package/cli/dist/run.js +1 -6
- package/cli/dist/status.js +1 -2
- package/package.json +1 -1
- package/standards/context-layout.md +3 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://anthropic.com/claude-code/plugin.schema.json",
|
|
3
3
|
"name": "zeno",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.13.0",
|
|
5
5
|
"description": "Keeps your domain knowledge and AI rules local and enforces them, so Claude hallucinates less.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Ritesh Firodiya",
|
package/README.md
CHANGED
|
@@ -162,34 +162,33 @@ wiki:
|
|
|
162
162
|
context:
|
|
163
163
|
folders: # added to the ten .context folders
|
|
164
164
|
- { folder: research, reason: interview notes, kept apart from raw/ }
|
|
165
|
-
copies: # files
|
|
165
|
+
copies: # files products copy from one source repo, in named sets
|
|
166
166
|
source: charades # a folder beside the estate config
|
|
167
|
-
|
|
168
|
-
- scripts/play-upload.mjs
|
|
169
|
-
-
|
|
170
|
-
|
|
167
|
+
sets:
|
|
168
|
+
play-upload: [scripts/play-upload.mjs] # the same path on both sides
|
|
169
|
+
design-shell-web:
|
|
170
|
+
- { from: .context/designs/mobile/index.html, to: .context/designs/web/index.html, named: true }
|
|
171
|
+
- { from: .context/designs/mobile/shared.css, to: .context/designs/web/shared.css, until: "PALETTE — " }
|
|
171
172
|
```
|
|
172
173
|
|
|
173
174
|
```yaml
|
|
174
175
|
# <repo>/.claude/zeno.yaml — written by `zeno init`
|
|
175
|
-
zeno: 0.
|
|
176
|
+
zeno: 0.13.0
|
|
176
177
|
kind: expo-app # expo-app | web | content
|
|
177
178
|
modules: [docs, knowledge, code, listing] # all optional; design is added when .context/designs exists, metrics only when asked
|
|
178
179
|
packs: [expo]
|
|
179
|
-
checks:
|
|
180
|
-
stale-copies: { enabled: false, reason: "Cursor is still used here" }
|
|
181
180
|
weigh:
|
|
182
181
|
weights: { C-05: 2 }
|
|
183
182
|
name: Chitragupt # how the product is written; `named` copies swap it in
|
|
184
183
|
copies:
|
|
185
|
-
|
|
186
|
-
- { file: scripts/release-android.mjs, reason: built on EAS from a tag }
|
|
184
|
+
use: [play-upload, design-shell-web] # the sets this product uses; only these are held to the source
|
|
187
185
|
```
|
|
188
186
|
|
|
189
187
|
Scalars: the most specific layer wins. `packs` and `standards` add up across
|
|
190
188
|
layers; `commands`, `doctor.tools`, `wiki.types` and `context.folders` merge
|
|
191
|
-
by name. Built-in types and folders cannot be removed or
|
|
192
|
-
|
|
189
|
+
by name; `copies.use` adds up. Built-in types and folders cannot be removed or
|
|
190
|
+
redefined. A repo names only what it uses: a check applies through the modules
|
|
191
|
+
it turns on, and there is no setting to skip one.
|
|
193
192
|
|
|
194
193
|
## Standards
|
|
195
194
|
|
|
@@ -17,6 +17,7 @@ export const commandChecks = {
|
|
|
17
17
|
run({ repo, config }) {
|
|
18
18
|
return config.commands
|
|
19
19
|
.filter((command) => !command.if || existsSync(join(repo, command.if)))
|
|
20
|
+
.filter((command) => !command.module || config.modules.includes(command.module))
|
|
20
21
|
.flatMap((command) => {
|
|
21
22
|
const result = spawnSync("sh", ["-c", command.run], { cwd: repo, encoding: "utf8" });
|
|
22
23
|
if (result.error) {
|
package/cli/dist/checks/refs.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { basename, dirname, join, normalize } from "node:path";
|
|
4
|
+
import { usedCopies } from "./shared-files.js";
|
|
4
5
|
// A `.context/` path not preceded by something that makes it part of a longer one (another
|
|
5
6
|
// product's, an absolute path, a URL). A leading `./` means "beside this file".
|
|
6
7
|
const PATH = /(?<![\w/~.-])(\.\/)?(\.context\/[\w./@+-]*)/g;
|
|
@@ -28,7 +29,8 @@ function literalPart(token, restOfLine) {
|
|
|
28
29
|
}
|
|
29
30
|
export const refs = {
|
|
30
31
|
id: "refs",
|
|
31
|
-
module:
|
|
32
|
+
// A module, not always on: a tool whose docs describe other repos' .context/ (zeno, canon) does not use it.
|
|
33
|
+
module: "refs",
|
|
32
34
|
description: "every .context/ path a tracked file names exists; a [[link]] names a page (a link to a page not yet written is a note)",
|
|
33
35
|
run({ repo, config }) {
|
|
34
36
|
const listed = git(repo, ["ls-files", "-z"]);
|
|
@@ -36,7 +38,7 @@ export const refs = {
|
|
|
36
38
|
return [];
|
|
37
39
|
const files = listed.stdout.split("\0").filter((file) => file && !BINARY.test(file));
|
|
38
40
|
// A copy of the source repo's file carries the source's paths; the product cannot edit it.
|
|
39
|
-
const copied = new Set(config.
|
|
41
|
+
const copied = new Set(usedCopies(config).map((pair) => pair.to));
|
|
40
42
|
const pages = new Set(files.filter((f) => f.startsWith(".context/wiki/") && f.endsWith(".md")).map((f) => basename(f, ".md")));
|
|
41
43
|
const features = new Set(files.flatMap((f) => (FEATURE_FILE.exec(f) ? [`FEAT-${FEATURE_FILE.exec(f)[1]}`] : [])));
|
|
42
44
|
const resolves = (target) => pages.has(target) || features.has(target) || existsSync(join(repo, ".context", `${target}.md`));
|
|
@@ -3,10 +3,10 @@ import { basename, join, resolve } from "node:path";
|
|
|
3
3
|
import { loadConfig } from "../config/load.js";
|
|
4
4
|
// zeno init writes the README heading `# .context — <Name>`; a product without a `name:` is read from it.
|
|
5
5
|
const README_NAME = /^# \.context — (.+)$/m;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
return (
|
|
6
|
+
const pairOf = (set, file) => typeof file === "string" ? { set, from: file, to: file, named: false } : { set, from: file.from, to: file.to ?? file.from, named: file.named ?? false, until: file.until };
|
|
7
|
+
/** Every file of every set the product uses, as source → product paths. */
|
|
8
|
+
export function usedCopies(config) {
|
|
9
|
+
return config.copies.use.flatMap((set) => (config.copies.sets[set] ?? []).map((file) => pairOf(set, file)));
|
|
10
10
|
}
|
|
11
11
|
function nameOf(repo, config) {
|
|
12
12
|
if (config.name)
|
|
@@ -14,82 +14,70 @@ function nameOf(repo, config) {
|
|
|
14
14
|
const readme = join(repo, ".context", "README.md");
|
|
15
15
|
return existsSync(readme) ? README_NAME.exec(readFileSync(readme, "utf8"))?.[1]?.trim() : undefined;
|
|
16
16
|
}
|
|
17
|
-
function
|
|
17
|
+
function split(text, marker) {
|
|
18
18
|
const lines = text.split("\n");
|
|
19
19
|
const at = lines.findIndex((line) => line.includes(marker));
|
|
20
|
-
return
|
|
20
|
+
return at === -1 ? null : { shared: lines.slice(0, at).join("\n"), own: lines.slice(at).join("\n") };
|
|
21
21
|
}
|
|
22
|
-
|
|
23
|
-
const lines = text.split("\n");
|
|
24
|
-
const at = lines.findIndex((line) => line.includes(marker));
|
|
25
|
-
return at === -1 ? "" : lines.slice(at).join("\n");
|
|
26
|
-
}
|
|
27
|
-
/** Compares every shared file of `repo` with the source repo. Skipped files and files absent from the source are left out. */
|
|
22
|
+
/** Compares every file of every set the product uses with the source repo. */
|
|
28
23
|
export function compareCopies(repo, config) {
|
|
29
|
-
const { source,
|
|
30
|
-
if (
|
|
24
|
+
const { source, sets, use } = config.copies;
|
|
25
|
+
if (use.length === 0 || !source || resolve(source) === resolve(repo))
|
|
31
26
|
return [];
|
|
32
|
-
const
|
|
27
|
+
const unknown = use.filter((set) => !sets[set]).map((set) => ({ pair: { set, from: "", to: "", named: false }, state: "unknown-set" }));
|
|
33
28
|
const productName = nameOf(repo, config);
|
|
34
29
|
const sourceName = nameOf(source, loadConfig(source).config);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (want === have)
|
|
59
|
-
states.push({ pair, state: "same" });
|
|
60
|
-
// Without the marker the product's own part cannot be told from the shared one, so nothing is written.
|
|
61
|
-
else if (pair.until && !current.includes(pair.until))
|
|
62
|
-
states.push({ pair, state: "drift" });
|
|
63
|
-
else
|
|
64
|
-
states.push({ pair, state: "drift", want: pair.until ? `${want}\n${below(current, pair.until)}` : want });
|
|
65
|
-
}
|
|
66
|
-
return states;
|
|
30
|
+
return [
|
|
31
|
+
...unknown,
|
|
32
|
+
...usedCopies(config).map((pair) => {
|
|
33
|
+
const from = join(source, pair.from);
|
|
34
|
+
const to = join(repo, pair.to);
|
|
35
|
+
if (!existsSync(from))
|
|
36
|
+
return { pair, state: "no-source" };
|
|
37
|
+
if (!existsSync(to))
|
|
38
|
+
return { pair, state: "missing" };
|
|
39
|
+
if (pair.named && (!productName || !sourceName))
|
|
40
|
+
return { pair, state: "unnamed" };
|
|
41
|
+
const original = pair.named ? readFileSync(from, "utf8").replaceAll(sourceName, productName) : readFileSync(from, "utf8");
|
|
42
|
+
const current = readFileSync(to, "utf8");
|
|
43
|
+
if (!pair.until)
|
|
44
|
+
return original === current ? { pair, state: "same" } : { pair, state: "drift", want: original };
|
|
45
|
+
const theirs = split(original, pair.until);
|
|
46
|
+
const ours = split(current, pair.until);
|
|
47
|
+
if (theirs && ours && theirs.shared === ours.shared)
|
|
48
|
+
return { pair, state: "same" };
|
|
49
|
+
// Without the marker the product's own part cannot be told from the shared one, so nothing is written.
|
|
50
|
+
return { pair, state: "drift", want: theirs && ours ? `${theirs.shared}\n${ours.own}` : undefined };
|
|
51
|
+
}),
|
|
52
|
+
];
|
|
67
53
|
}
|
|
68
54
|
/** Overwrites each drifted copy with the source's. Never adds a missing file. Returns the paths written. */
|
|
69
55
|
export function syncCopies(repo, config) {
|
|
70
|
-
|
|
71
|
-
for (const { pair, state, want } of compareCopies(repo, config)) {
|
|
56
|
+
return compareCopies(repo, config).flatMap(({ pair, state, want }) => {
|
|
72
57
|
if (state !== "drift" || want === undefined)
|
|
73
|
-
|
|
58
|
+
return [];
|
|
74
59
|
writeFileSync(join(repo, pair.to), want);
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
return written;
|
|
60
|
+
return [pair.to];
|
|
61
|
+
});
|
|
78
62
|
}
|
|
79
63
|
export const sharedFiles = {
|
|
80
64
|
id: "shared-files",
|
|
81
65
|
module: null,
|
|
82
|
-
description: "
|
|
66
|
+
description: "every file of every copies set the product uses exists and matches copies.source",
|
|
83
67
|
run({ repo, config }) {
|
|
84
|
-
const source = config.copies.source ? basename(config.copies.source) : "";
|
|
68
|
+
const source = config.copies.source ? basename(config.copies.source) : "copies.source";
|
|
85
69
|
return compareCopies(repo, config).flatMap(({ pair, state }) => {
|
|
86
|
-
const finding = (message) => [{ check: "shared-files", level: "error", file
|
|
70
|
+
const finding = (file, message) => [{ check: "shared-files", level: "error", file, message }];
|
|
71
|
+
if (state === "unknown-set")
|
|
72
|
+
return finding(".claude/zeno.yaml", `copies.use names ${pair.set}, which no copies.sets defines`);
|
|
73
|
+
if (state === "no-source")
|
|
74
|
+
return finding(pair.to, `${source} has no ${pair.from} (set ${pair.set})`);
|
|
87
75
|
if (state === "missing")
|
|
88
|
-
return finding(`missing —
|
|
76
|
+
return finding(pair.to, `missing — set ${pair.set} is in use; copy it from ${source}`);
|
|
89
77
|
if (state === "unnamed")
|
|
90
|
-
return finding(
|
|
78
|
+
return finding(pair.to, "no product name to compare with — set name: in zeno.yaml");
|
|
91
79
|
if (state === "drift")
|
|
92
|
-
return finding(`differs from ${source}${pair.until ? " above the shared block's end" : ""} — \`zeno copies --sync\` copies it`);
|
|
80
|
+
return finding(pair.to, `differs from ${source}${pair.until ? " above the shared block's end" : ""} — \`zeno copies --sync\` copies it`);
|
|
93
81
|
return [];
|
|
94
82
|
});
|
|
95
83
|
},
|
|
@@ -14,13 +14,13 @@ function accessValue(body, id) {
|
|
|
14
14
|
export const sources = {
|
|
15
15
|
id: "sources",
|
|
16
16
|
module: "docs",
|
|
17
|
-
description: "each SOURCE.* page says how to read it (mcp, skill, manual, or a cli in doctor.tools) and has a How to query section",
|
|
17
|
+
description: "each SOURCE.* entity page says how to read it (mcp, skill, manual, or a cli in doctor.tools) and has a How to query section",
|
|
18
18
|
run({ repo, config }) {
|
|
19
19
|
const tools = new Set(config.doctor.tools.map((tool) => tool.tool));
|
|
20
20
|
return wikiPages(repo).flatMap((file) => {
|
|
21
21
|
const { data, body } = parseFrontmatter(readFileSync(join(repo, file), "utf8"));
|
|
22
22
|
const id = String(data?.id ?? "");
|
|
23
|
-
if (
|
|
23
|
+
if (!file.startsWith(".context/wiki/entities/") || !id.startsWith("SOURCE."))
|
|
24
24
|
return [];
|
|
25
25
|
const findings = [];
|
|
26
26
|
const add = (message) => findings.push({ check: "sources", level: "error", file, message });
|
package/cli/dist/config/load.js
CHANGED
|
@@ -85,7 +85,6 @@ export function loadConfig(repo, options = {}) {
|
|
|
85
85
|
standards: [...new Set(layers.flatMap((layer) => (layer.data.standards ?? []).map((dir) => resolve(layer.base, dir))))],
|
|
86
86
|
commands: mergeBy(pick("commands").map((list) => list ?? []), (command) => command.id),
|
|
87
87
|
doctor: { tools: mergeBy(pick("doctor").map((doctor) => doctor?.tools ?? []), (tool) => tool.tool) },
|
|
88
|
-
checks: Object.assign({}, ...pick("checks")),
|
|
89
88
|
weigh: { weights: Object.assign({}, ...pick("weigh").map((weigh) => weigh?.weights)) },
|
|
90
89
|
wiki: { types: mergeBy(pick("wiki").map((wiki) => wiki?.types ?? []), (entry) => entry.type) },
|
|
91
90
|
context: { folders: mergeBy(pick("context").map((context) => context?.folders ?? []), (entry) => entry.folder) },
|
|
@@ -93,8 +92,8 @@ export function loadConfig(repo, options = {}) {
|
|
|
93
92
|
name: lastDefined(pick("name")),
|
|
94
93
|
copies: {
|
|
95
94
|
source: lastDefined(layers.map((layer) => (layer.data.copies?.source ? resolve(layer.base, layer.data.copies.source) : undefined))),
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
sets: Object.assign({}, ...pick("copies").map((copies) => copies?.sets)),
|
|
96
|
+
use: [...new Set(pick("copies").flatMap((copies) => copies?.use ?? []))],
|
|
98
97
|
},
|
|
99
98
|
};
|
|
100
99
|
if (config.name === undefined)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
export const KINDS = ["expo-app", "web", "content"];
|
|
3
|
-
export const MODULES = ["docs", "design", "knowledge", "metrics", "code", "architecture", "listing"];
|
|
3
|
+
export const MODULES = ["docs", "design", "knowledge", "metrics", "code", "architecture", "listing", "refs"];
|
|
4
4
|
/**
|
|
5
5
|
* The wiki page types every repo has, and the folder each lives in. A repo adds more under `wiki.types`.
|
|
6
6
|
* No flow type: a journey worth keeping is a concept. A surface page is a screen's notes, only where it has real ones.
|
|
@@ -18,11 +18,6 @@ export const REMOVED_WIKI_FOLDERS = {
|
|
|
18
18
|
};
|
|
19
19
|
/** The `.context/` folders every repo may have. A repo adds more under `context.folders`. */
|
|
20
20
|
export const CONTEXT_FOLDERS = ["raw", "wiki", "features", "designs", "listing", "marketing", "metrics", "support", "legal", "ops"];
|
|
21
|
-
const checkOverride = z
|
|
22
|
-
.object({ enabled: z.boolean().default(true), reason: z.string().min(1).optional() })
|
|
23
|
-
.refine((check) => check.enabled || check.reason !== undefined, {
|
|
24
|
-
message: "a check turned off needs a `reason:` — an override nobody can explain is drift",
|
|
25
|
-
});
|
|
26
21
|
const principleId = z.string().regex(/^C-\d{2}$/, "principle IDs look like C-07");
|
|
27
22
|
const command = z
|
|
28
23
|
.object({
|
|
@@ -31,6 +26,8 @@ const command = z
|
|
|
31
26
|
run: z.string().min(1),
|
|
32
27
|
/** Only run when this path exists in the repo. */
|
|
33
28
|
if: z.string().optional(),
|
|
29
|
+
/** Only run in a repo that uses this module. */
|
|
30
|
+
module: z.enum(MODULES).optional(),
|
|
34
31
|
})
|
|
35
32
|
.strict();
|
|
36
33
|
const kebab = z.string().regex(/^[a-z0-9-]+$/, "use lowercase-kebab");
|
|
@@ -49,17 +46,13 @@ const wikiType = z
|
|
|
49
46
|
message: "a built-in wiki type or its folder cannot be redefined — add a new one",
|
|
50
47
|
});
|
|
51
48
|
const contextFolder = z.object({ folder: kebab, reason }).strict();
|
|
52
|
-
/**
|
|
53
|
-
* A file every product carries a copy of. A plain path is the same path on both sides and every
|
|
54
|
-
* product must carry it or skip it with a reason; an object maps one source file onto product
|
|
55
|
-
* paths that exist only where the product has that part (a design surface, say).
|
|
56
|
-
*/
|
|
49
|
+
/** A file copied from the source repo: the same path on both sides, or `from` one path `to` another. */
|
|
57
50
|
const copyFile = z.union([
|
|
58
51
|
z.string().min(1),
|
|
59
52
|
z
|
|
60
53
|
.object({
|
|
61
54
|
from: z.string().min(1),
|
|
62
|
-
to: z.
|
|
55
|
+
to: z.string().min(1).optional(),
|
|
63
56
|
/** Identical except that the source product's name reads as this product's. */
|
|
64
57
|
named: z.boolean().optional(),
|
|
65
58
|
/** Compared only above the first line containing this text; below it is the product's own. */
|
|
@@ -67,7 +60,6 @@ const copyFile = z.union([
|
|
|
67
60
|
})
|
|
68
61
|
.strict(),
|
|
69
62
|
]);
|
|
70
|
-
const copySkip = z.object({ file: z.string().min(1), reason }).strict();
|
|
71
63
|
const stage = z
|
|
72
64
|
.object({
|
|
73
65
|
id: kebab,
|
|
@@ -92,7 +84,6 @@ export const layerSchema = z
|
|
|
92
84
|
standards: z.array(z.string()),
|
|
93
85
|
commands: z.array(command),
|
|
94
86
|
doctor: z.object({ tools: z.array(tool) }).partial(),
|
|
95
|
-
checks: z.record(z.string(), checkOverride),
|
|
96
87
|
weigh: z.object({ weights: z.record(principleId, z.number().nonnegative()) }).partial(),
|
|
97
88
|
wiki: z.object({ types: z.array(wikiType) }).partial(),
|
|
98
89
|
context: z.object({ folders: z.array(contextFolder) }).partial(),
|
|
@@ -100,13 +91,16 @@ export const layerSchema = z
|
|
|
100
91
|
pipeline: z.array(stage),
|
|
101
92
|
/** How the product is written in prose, for files shared with only the name changed. */
|
|
102
93
|
name: z.string().min(1),
|
|
103
|
-
/**
|
|
104
|
-
|
|
94
|
+
/**
|
|
95
|
+
* Files copied from one source repo. The estate names sets of them; a product lists the sets it
|
|
96
|
+
* uses, and only those are held equal to the source.
|
|
97
|
+
*/
|
|
98
|
+
copies: z.object({ source: z.string().min(1), sets: z.record(kebab, z.array(copyFile).min(1)), use: z.array(kebab) }).partial().strict(),
|
|
105
99
|
})
|
|
106
100
|
.partial()
|
|
107
101
|
.strict();
|
|
108
102
|
export const MODULES_BY_KIND = {
|
|
109
|
-
"expo-app": ["docs", "knowledge", "code", "listing"],
|
|
110
|
-
web: ["docs", "knowledge", "code"],
|
|
111
|
-
content: ["docs", "knowledge"],
|
|
103
|
+
"expo-app": ["docs", "knowledge", "code", "listing", "refs"],
|
|
104
|
+
web: ["docs", "knowledge", "code", "refs"],
|
|
105
|
+
content: ["docs", "knowledge", "refs"],
|
|
112
106
|
};
|
package/cli/dist/format.js
CHANGED
|
@@ -18,7 +18,7 @@ export function formatReports(reports, options = {}) {
|
|
|
18
18
|
errors += count.errors;
|
|
19
19
|
warnings += count.warnings;
|
|
20
20
|
const clean = report.findings.length === 0;
|
|
21
|
-
if (clean && options.quiet
|
|
21
|
+
if (clean && options.quiet)
|
|
22
22
|
continue;
|
|
23
23
|
const summary = clean ? "ok" : `${plural(count.errors, "error")}, ${plural(count.warnings, "warning")}`;
|
|
24
24
|
lines.push(`${report.name} ${summary}`);
|
|
@@ -26,8 +26,6 @@ export function formatReports(reports, options = {}) {
|
|
|
26
26
|
const message = finding.message.replace(/\n/g, "\n ");
|
|
27
27
|
lines.push(` ${MARK[finding.level]} ${finding.check} ${finding.file ?? ""} ${message}`.replace(/ +$/, ""));
|
|
28
28
|
}
|
|
29
|
-
for (const override of report.overrides)
|
|
30
|
-
lines.push(` override ${override.check}: ${override.reason}`);
|
|
31
29
|
}
|
|
32
30
|
lines.push(`${plural(reports.length, "product")} · ${plural(errors, "error")} · ${plural(warnings, "warning")}`);
|
|
33
31
|
return lines.join("\n");
|
package/cli/dist/init.js
CHANGED
|
@@ -69,7 +69,7 @@ const FIRST_FILE = {
|
|
|
69
69
|
function template(file) {
|
|
70
70
|
switch (file) {
|
|
71
71
|
case ".context/README.md":
|
|
72
|
-
return "# .context\n\nProduct material, in the
|
|
72
|
+
return "# .context\n\nProduct material, in the zeno context layout. Start at [`tasks.md`](tasks.md).\n";
|
|
73
73
|
case ".context/tasks.md":
|
|
74
74
|
return "# Tasks\n\n## Now\n\n## Blocked\n\n## Waiting\n";
|
|
75
75
|
case ".context/wiki/index.md":
|
package/cli/dist/run.js
CHANGED
|
@@ -4,7 +4,7 @@ import { ALL_CHECKS } from "./checks/index.js";
|
|
|
4
4
|
import { loadConfig } from "./config/load.js";
|
|
5
5
|
import { isDirectory } from "./lib/files.js";
|
|
6
6
|
export function runChecks(repo, options) {
|
|
7
|
-
const report = { repo, name: basename(repo), findings: [], skipped: []
|
|
7
|
+
const report = { repo, name: basename(repo), findings: [], skipped: [] };
|
|
8
8
|
if (!isDirectory(repo)) {
|
|
9
9
|
report.findings.push({ check: "repo", level: "error", message: `no folder at ${repo}` });
|
|
10
10
|
return report;
|
|
@@ -18,11 +18,6 @@ export function runChecks(repo, options) {
|
|
|
18
18
|
return report;
|
|
19
19
|
}
|
|
20
20
|
for (const check of options.checks ?? ALL_CHECKS) {
|
|
21
|
-
const override = config.checks[check.id];
|
|
22
|
-
if (override && !override.enabled) {
|
|
23
|
-
report.overrides.push({ check: check.id, reason: override.reason ?? "" });
|
|
24
|
-
continue;
|
|
25
|
-
}
|
|
26
21
|
if (check.module && !config.modules.includes(check.module)) {
|
|
27
22
|
report.skipped.push({ check: check.id, why: `module ${check.module} is off` });
|
|
28
23
|
continue;
|
package/cli/dist/status.js
CHANGED
|
@@ -21,7 +21,6 @@ export function buildStatus(repo, options) {
|
|
|
21
21
|
`kind ${config.kind}`,
|
|
22
22
|
`modules ${config.modules.join(", ")}`,
|
|
23
23
|
`config ${configLine}`,
|
|
24
|
-
`checks ${errors} error${errors === 1 ? "" : "s"}, ${warnings} warning${warnings === 1 ? "" : "s"}
|
|
25
|
-
(report.overrides.length > 0 ? `, ${report.overrides.length} override(s)` : ""),
|
|
24
|
+
`checks ${errors} error${errors === 1 ? "" : "s"}, ${warnings} warning${warnings === 1 ? "" : "s"}`,
|
|
26
25
|
].join("\n");
|
|
27
26
|
}
|
package/package.json
CHANGED
|
@@ -7,7 +7,7 @@ folder.
|
|
|
7
7
|
|
|
8
8
|
```
|
|
9
9
|
<repo>/.context/
|
|
10
|
-
├── README.md what this is, and
|
|
10
|
+
├── README.md what this is, and where to start
|
|
11
11
|
├── tasks.md the ordered queue. The top item is the next action.
|
|
12
12
|
├── raw/ sources you didn't write. Immutable.
|
|
13
13
|
├── wiki/ everything you concluded. Schema: wiki-schema.md
|
|
@@ -30,7 +30,8 @@ folder.
|
|
|
30
30
|
with the reason: `context: { folders: [{ folder: research, reason: … }] }`.
|
|
31
31
|
The ten cannot be removed.
|
|
32
32
|
2. **`README.md` and `tasks.md` are the only files at `.context/` root.**
|
|
33
|
-
|
|
33
|
+
A repo names only what it uses. Its README, its `zeno.yaml` and its docs never
|
|
34
|
+
list a folder, file or tool it does without; absence is shown by absence.
|
|
34
35
|
3. **Nothing is created empty.** A folder appears with its first file — git does
|
|
35
36
|
not track empty folders, so a placeholder disappears on clone. `raw/` is the
|
|
36
37
|
one exception and carries a `.gitkeep`.
|