@fakto/core 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -0
- package/bin/fakto.mjs +26 -3
- package/lib/core.mjs +9 -1
- package/lib/skills.mjs +95 -0
- package/package.json +3 -1
- package/skills/fakto/SKILL.md +131 -0
- package/skills/fakto-canvas/SKILL.md +40 -0
package/README.md
CHANGED
|
@@ -69,6 +69,22 @@ npx fakto canvas # the live map on http://localhost:4848 (--backgrou
|
|
|
69
69
|
|
|
70
70
|
As a library: `import { loadRepo, checkRepo, staleList } from '@fakto/core'`.
|
|
71
71
|
|
|
72
|
+
## Claude Code skills
|
|
73
|
+
|
|
74
|
+
The package ships the two skills Claude uses in a Fakto repo (`fakto`: the change workflow;
|
|
75
|
+
`fakto-canvas`: starts the live map) and the hooks that run the check after every edit and
|
|
76
|
+
start the canvas at session start. Claude Code loads skills from the project, not from
|
|
77
|
+
`node_modules`, so install them into the repo:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npx fakto skills # write .claude/skills/* and merge the hooks into .claude/settings.json
|
|
81
|
+
npx fakto skills --check # exit 1 when they are missing, edited, or from another version
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Put `"postinstall": "fakto skills"` in the repo's package.json so every `npm install` refreshes
|
|
85
|
+
them; the session-start hook runs it too, and `fakto check` warns when they are not current.
|
|
86
|
+
Commit them: they are readable on GitHub and work before the first install.
|
|
87
|
+
|
|
72
88
|
## Canvas
|
|
73
89
|
|
|
74
90
|
A local web app that draws the whole graph, lets you edit or add a fact, and runs your
|
package/bin/fakto.mjs
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* fakto touch <file | fact-id>… stamp reviewed: today and the source hashes (after a human reviewed it)
|
|
12
12
|
* fakto sync [path | fact-id …] bring facts and sections in line with the files they draw on (--all)
|
|
13
13
|
* fakto canvas open the canvas on localhost (--port 4848; --background to detach, --stop)
|
|
14
|
+
* fakto skills install or refresh the Claude Code skills and hooks in .claude/ (--check, --quiet)
|
|
14
15
|
*
|
|
15
16
|
* Conventions (full spec in this package's README):
|
|
16
17
|
* facts <facts>/*.yaml, flat records: - id / value / statement / status / updated
|
|
@@ -26,7 +27,7 @@
|
|
|
26
27
|
*
|
|
27
28
|
* All logic lives in ../lib; this file only formats and dispatches.
|
|
28
29
|
*/
|
|
29
|
-
import { findRoot, loadRepo, staleList, checkRepo, graphJson, touch as touchFiles, renderOutputs, renderStatus, syncPlan, syncExamples, feedsIndex, previousSelection, lineDiff } from '../lib/core.mjs';
|
|
30
|
+
import { installSkills, skillsStatus, findRoot, loadRepo, staleList, checkRepo, graphJson, touch as touchFiles, renderOutputs, renderStatus, syncPlan, syncExamples, feedsIndex, previousSelection, lineDiff } from '../lib/core.mjs';
|
|
30
31
|
|
|
31
32
|
const ROOT = findRoot(process.cwd());
|
|
32
33
|
const cmd = process.argv[2];
|
|
@@ -164,6 +165,27 @@ function render(repo, only) {
|
|
|
164
165
|
if (done.some((r) => !r.ok)) process.exit(1);
|
|
165
166
|
}
|
|
166
167
|
|
|
168
|
+
/**
|
|
169
|
+
* fakto skills [--check | --quiet]
|
|
170
|
+
* Installs this package's Claude Code skills into .claude/skills (stamped with the version) and
|
|
171
|
+
* merges its hooks and permissions into .claude/settings.json. Runs on postinstall and at session
|
|
172
|
+
* start. --check exits 1 when anything is missing or out of date; --quiet prints only changes.
|
|
173
|
+
*/
|
|
174
|
+
function skills() {
|
|
175
|
+
if (args.includes('--check')) {
|
|
176
|
+
const s = skillsStatus(ROOT);
|
|
177
|
+
for (const k of s.skills) console.log(`${k.status === 'ok' ? grn('ok ') : red(k.status.padEnd(8))} .claude/skills/${k.name}${k.installed ? dim(` (installed ${k.installed}, package ${s.version})`) : ''}`);
|
|
178
|
+
console.log(`${s.settings === 'ok' ? grn('ok ') : red(s.settings.padEnd(8))} .claude/settings.json`);
|
|
179
|
+
if (!s.ok) { console.log(`\nRun ${'npx fakto skills'} to install the skills for ${s.version}.`); process.exit(1); }
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const { changed, version } = installSkills(ROOT);
|
|
183
|
+
const quiet = args.includes('--quiet');
|
|
184
|
+
if (!changed.length) { if (!quiet) console.log(grn(`Fakto skills are up to date (${version}).`)); return; }
|
|
185
|
+
if (quiet) console.log(`The Fakto skills were just updated to ${version} (${changed.join(', ')}). Read .claude/skills/fakto/SKILL.md and .claude/skills/fakto-canvas/SKILL.md again before you use them.`);
|
|
186
|
+
else for (const c of changed) console.log(`${c}: updated to ${version}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
167
189
|
/** What answers on a port: { root } for a fakto canvas, 'other' for anything else, null when free. */
|
|
168
190
|
async function probe(port) {
|
|
169
191
|
try {
|
|
@@ -234,7 +256,7 @@ async function canvas() {
|
|
|
234
256
|
await start({ root: ROOT, port });
|
|
235
257
|
}
|
|
236
258
|
|
|
237
|
-
const repo = cmd && cmd !== 'canvas' ? loadRepo(ROOT) : null;
|
|
259
|
+
const repo = cmd && cmd !== 'canvas' && cmd !== 'skills' ? loadRepo(ROOT) : null;
|
|
238
260
|
switch (cmd) {
|
|
239
261
|
case 'graph': graph(repo); break;
|
|
240
262
|
case 'check': check(repo); break;
|
|
@@ -243,8 +265,9 @@ switch (cmd) {
|
|
|
243
265
|
case 'touch': touch(repo, args); break;
|
|
244
266
|
case 'render': render(repo, args); break;
|
|
245
267
|
case 'sync': sync(repo, args); break;
|
|
268
|
+
case 'skills': skills(); break;
|
|
246
269
|
case 'canvas': await canvas(); break;
|
|
247
270
|
default:
|
|
248
|
-
console.log('usage: fakto graph | check | stale | show <fact-id | file> | touch <file | fact-id>… | render [path…] | sync [path | fact-id …] | canvas');
|
|
271
|
+
console.log('usage: fakto graph | check | stale | show <fact-id | file> | touch <file | fact-id>… | render [path…] | sync [path | fact-id …] | skills | canvas');
|
|
249
272
|
process.exit(cmd ? 1 : 0);
|
|
250
273
|
}
|
package/lib/core.mjs
CHANGED
|
@@ -18,8 +18,10 @@
|
|
|
18
18
|
import fs from 'node:fs';
|
|
19
19
|
import path from 'node:path';
|
|
20
20
|
import { renderStatus } from './render.mjs';
|
|
21
|
+
import { skillsStatus } from './skills.mjs';
|
|
21
22
|
import { parseSourceAnchors, factSourceStatus, drawStatus, exampleStatus, syncPlan, sourceCycles, restampFile, restampedSource, parseRef } from './sources.mjs';
|
|
22
23
|
|
|
24
|
+
export { installSkills, skillsStatus, PACKAGE } from './skills.mjs';
|
|
23
25
|
export { renderStatus, renderOutputs, parseSlides, stripAnchors, sourceHash, RENDERERS } from './render.mjs';
|
|
24
26
|
export { parseRef, refString, resolveRef, blobHash, factSourceStatus, drawStatus, exampleStatus, syncPlan, sourceCycles, syncExamples, previousSelection, lineDiff, FROM_RE, EXAMPLE_RE } from './sources.mjs';
|
|
25
27
|
|
|
@@ -162,7 +164,7 @@ export function anchorOk(repo, artifact, anchor) {
|
|
|
162
164
|
* Every problem, in the order the CLI prints them.
|
|
163
165
|
* level: error | warning | info. kind: orphan | no-stamp | unknown | unknown-support | retired | draft | drift | stale |
|
|
164
166
|
* render-stale | render-missing | render-config | source-changed | source-missing | example-drift | source-cycle |
|
|
165
|
-
* region-unused.
|
|
167
|
+
* region-unused | skills.
|
|
166
168
|
* `message` is the CLI's text (without colour); line-level problems also carry
|
|
167
169
|
* path, line, text, fact for the canvas.
|
|
168
170
|
*/
|
|
@@ -221,6 +223,12 @@ export function checkRepo(repo) {
|
|
|
221
223
|
for (const a of repo.artifacts) for (const x of [...a.draws, ...a.examples]) if (x.ref.selector) used.add(`${x.ref.path}#${x.ref.selector}`);
|
|
222
224
|
for (const a of repo.artifacts) for (const r of a.regions) if (!used.has(`${a.file}#${r.name}`)) out.push({ level: 'info', kind: 'region-unused', path: a.file, line: r.line, message: `${a.file}:${r.line}: region "${r.name}" is marked but nothing draws on it` });
|
|
223
225
|
}
|
|
226
|
+
// The Claude Code skills and hooks this package ships must be installed, and current.
|
|
227
|
+
const sk = skillsStatus(repo.root);
|
|
228
|
+
if (!sk.ok) {
|
|
229
|
+
const bad = [...sk.skills.filter((s) => s.status !== 'ok').map((s) => `${s.name} ${s.status}${s.installed ? ` (${s.installed})` : ''}`), ...(sk.settings === 'ok' ? [] : [`settings ${sk.settings}`])];
|
|
230
|
+
out.push({ level: 'warning', kind: 'skills', path: '.claude', message: `.claude: the Fakto skills for Claude Code are not current for ${sk.version} (${bad.join(', ')}) — run npx fakto skills` });
|
|
231
|
+
}
|
|
224
232
|
for (const r of renderStatus(repo)) {
|
|
225
233
|
if (r.status === 'stale') out.push({ level: 'warning', kind: 'render-stale', path: r.to, message: `${r.to}: out of date with ${r.from} — run fakto render` });
|
|
226
234
|
if (r.status === 'missing') out.push({ level: 'warning', kind: 'render-missing', path: r.to, message: `${r.to}: not rendered yet from ${r.from} — run fakto render` });
|
package/lib/skills.mjs
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Claude Code side of a Fakto repo ships in this package, so it can never drift from the
|
|
3
|
+
* CLI it calls: the skills in ../skills and the hooks and permissions below.
|
|
4
|
+
*
|
|
5
|
+
* installSkills(root) write .claude/skills/<name>/ (stamped with this package's version) and
|
|
6
|
+
* merge the hooks and permissions into .claude/settings.json
|
|
7
|
+
* skillsStatus(root) what `fakto skills --check` and `fakto check` report
|
|
8
|
+
*
|
|
9
|
+
* Claude Code loads skills from the project's .claude/skills, never from node_modules, so a
|
|
10
|
+
* repo runs `fakto skills` on postinstall and the session-start hook runs it again.
|
|
11
|
+
*/
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
|
|
16
|
+
const PKG = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
17
|
+
const SOURCE = path.join(PKG, 'skills');
|
|
18
|
+
export const PACKAGE = JSON.parse(fs.readFileSync(path.join(PKG, 'package.json'), 'utf8'));
|
|
19
|
+
const STAMP_RE = /<!-- Installed by `fakto skills` from (\S+)@(\d+\.\d+\.\d+[^\s.]*)\./;
|
|
20
|
+
const stamp = () => `\n<!-- Installed by \`fakto skills\` from ${PACKAGE.name}@${PACKAGE.version}. Do not edit: \`fakto skills\` overwrites this file; change the package's skills instead. -->\n`;
|
|
21
|
+
|
|
22
|
+
export const SESSION_HOOK = [
|
|
23
|
+
'[ -f fakto.config.json ] && [ -z "$FAKTO_CANVAS_RUN" ] && {',
|
|
24
|
+
'if [ -x node_modules/.bin/fakto ]; then node_modules/.bin/fakto skills --quiet;',
|
|
25
|
+
"echo 'This is a Fakto repo. Before anything else, use the fakto-canvas skill: start the canvas (npx fakto canvas --background) and tell the user where it runs.';",
|
|
26
|
+
"else echo 'This is a Fakto repo whose dependencies are not installed yet. Before anything else run npm install: it installs Fakto and refreshes its Claude skills in .claude/skills. Then use the fakto-canvas skill to start the canvas and tell the user where it runs.';",
|
|
27
|
+
'fi; } || true',
|
|
28
|
+
].join(' ');
|
|
29
|
+
export const CHECK_HOOK = 'npx --no fakto check 2>&1 | tail -n 8';
|
|
30
|
+
export const PERMISSIONS = ['Bash(npx fakto:*)', 'Bash(npm run:*)', 'Bash(npm install)'];
|
|
31
|
+
|
|
32
|
+
const ours = (entry) => (entry.hooks || []).some((h) => typeof h.command === 'string' && /\bfakto\b/.test(h.command));
|
|
33
|
+
const WANT_HOOKS = {
|
|
34
|
+
SessionStart: { hooks: [{ type: 'command', command: SESSION_HOOK }] },
|
|
35
|
+
PostToolUse: { matcher: 'Edit|Write|MultiEdit', hooks: [{ type: 'command', command: CHECK_HOOK }] },
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** The settings with Fakto's hooks and permissions in place; everything else is kept as it was. */
|
|
39
|
+
function mergedSettings(current) {
|
|
40
|
+
const s = JSON.parse(JSON.stringify(current || {}));
|
|
41
|
+
s.permissions = s.permissions || {};
|
|
42
|
+
s.permissions.allow = [...new Set([...(s.permissions.allow || []), ...PERMISSIONS])];
|
|
43
|
+
s.hooks = s.hooks || {};
|
|
44
|
+
for (const [event, entry] of Object.entries(WANT_HOOKS)) {
|
|
45
|
+
s.hooks[event] = [...(s.hooks[event] || []).filter((e) => !ours(e)), entry];
|
|
46
|
+
}
|
|
47
|
+
return s;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const skillNames = () => fs.readdirSync(SOURCE, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort();
|
|
51
|
+
const wanted = (name) => fs.readFileSync(path.join(SOURCE, name, 'SKILL.md'), 'utf8').replace(/\n*$/, '\n') + stamp();
|
|
52
|
+
const settingsPath = (root) => path.join(root, '.claude', 'settings.json');
|
|
53
|
+
const readJson = (p) => { try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return null; } };
|
|
54
|
+
|
|
55
|
+
/** { ok, version, skills: [{ name, status: ok | missing | outdated | modified, installed? }], settings: ok | missing | outdated } */
|
|
56
|
+
export function skillsStatus(root) {
|
|
57
|
+
const skills = skillNames().map((name) => {
|
|
58
|
+
const p = path.join(root, '.claude', 'skills', name, 'SKILL.md');
|
|
59
|
+
if (!fs.existsSync(p)) return { name, status: 'missing' };
|
|
60
|
+
const have = fs.readFileSync(p, 'utf8');
|
|
61
|
+
const m = have.match(STAMP_RE);
|
|
62
|
+
if (have === wanted(name)) return { name, status: 'ok' };
|
|
63
|
+
if (m && m[2] !== PACKAGE.version) return { name, status: 'outdated', installed: m[2] };
|
|
64
|
+
return { name, status: 'modified' }; // same version (or no stamp at all) but different text: edited by hand
|
|
65
|
+
});
|
|
66
|
+
const cur = readJson(settingsPath(root));
|
|
67
|
+
const settings = !cur ? 'missing' : JSON.stringify(mergedSettings(cur)) === JSON.stringify(cur) ? 'ok' : 'outdated';
|
|
68
|
+
return { ok: skills.every((s) => s.status === 'ok') && settings === 'ok', version: PACKAGE.version, skills, settings };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Write what is not ok. Returns { changed: [paths], version }. */
|
|
72
|
+
export function installSkills(root) {
|
|
73
|
+
const changed = [];
|
|
74
|
+
for (const name of skillNames()) {
|
|
75
|
+
const dir = path.join(root, '.claude', 'skills', name);
|
|
76
|
+
const p = path.join(dir, 'SKILL.md');
|
|
77
|
+
const want = wanted(name);
|
|
78
|
+
// a symlink (an older setup) is replaced by a real file, so the repo works on its own
|
|
79
|
+
try { if (fs.lstatSync(dir).isSymbolicLink()) fs.rmSync(dir); } catch { /* not there yet */ }
|
|
80
|
+
if (fs.existsSync(p) && fs.readFileSync(p, 'utf8') === want) continue;
|
|
81
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
82
|
+
fs.writeFileSync(p, want);
|
|
83
|
+
changed.push(path.relative(root, p));
|
|
84
|
+
}
|
|
85
|
+
const sp = settingsPath(root);
|
|
86
|
+
const cur = readJson(sp);
|
|
87
|
+
if (fs.existsSync(sp) && !cur) throw new Error(`${path.relative(root, sp)} is not valid JSON; fix it, then run fakto skills again`);
|
|
88
|
+
const next = mergedSettings(cur);
|
|
89
|
+
if (!cur || JSON.stringify(next) !== JSON.stringify(cur)) {
|
|
90
|
+
fs.mkdirSync(path.dirname(sp), { recursive: true });
|
|
91
|
+
fs.writeFileSync(sp, JSON.stringify(next, null, 2) + '\n');
|
|
92
|
+
changed.push(path.relative(root, sp));
|
|
93
|
+
}
|
|
94
|
+
return { changed, version: PACKAGE.version };
|
|
95
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fakto/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"license": "UNLICENSED",
|
|
5
5
|
"description": "Keep every file that states a fact in line with the fact: the CLI, the library and the canvas in one package.",
|
|
6
6
|
"type": "module",
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
"lib/core.mjs",
|
|
18
18
|
"lib/render.mjs",
|
|
19
19
|
"lib/sources.mjs",
|
|
20
|
+
"lib/skills.mjs",
|
|
21
|
+
"skills",
|
|
20
22
|
"canvas/server",
|
|
21
23
|
"canvas/dist",
|
|
22
24
|
"canvas/README.md",
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: fakto
|
|
3
|
+
description: >
|
|
4
|
+
Keep every artifact (docs, code, website, decks, box copy) in sync with the facts
|
|
5
|
+
it depends on. Use whenever a fact changes ("the price is now X", "we have 9 levels
|
|
6
|
+
now", "retire feature Y"), whenever `fakto check` or `fakto stale` reports something,
|
|
7
|
+
when adding a new fact or a new artifact, or when the user says "propagate",
|
|
8
|
+
"update everything that mentions X", or "what depends on X". Also when a source file
|
|
9
|
+
changed ("I changed the code, sync the docs", `fakto check` says "behind its source",
|
|
10
|
+
`fakto sync`). Facts live in the facts directory named in fakto.config.json; artifacts
|
|
11
|
+
carry `fact:<id>` anchors, and may draw on other files with `from:` / `example:`.
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# fakto — keep dependents in sync
|
|
15
|
+
|
|
16
|
+
Facts are the source of truth. Artifacts cite them with an inline anchor
|
|
17
|
+
`fact:<id>` on the line that states the fact. The CLI finds what depends on
|
|
18
|
+
what; this skill does the part that needs judgment: rewriting the dependents.
|
|
19
|
+
|
|
20
|
+
Run the CLI as `npx fakto <cmd>` (the repo installs the `fakto` package).
|
|
21
|
+
The conventions are in the package README (`node_modules/@fakto/core/README.md`).
|
|
22
|
+
If `fakto.config.json` names `houseRules`, read that file before writing any
|
|
23
|
+
artifact and follow it: it is the company's voice and wording rules.
|
|
24
|
+
|
|
25
|
+
## Which case are you in?
|
|
26
|
+
|
|
27
|
+
### A. A fact changed (the common case)
|
|
28
|
+
|
|
29
|
+
1. Edit the fact record only: `value`, `statement`, and bump `updated` to today.
|
|
30
|
+
Never touch an artifact first.
|
|
31
|
+
2. Run `npx fakto stale`. It lists every artifact whose `reviewed` date is older than
|
|
32
|
+
the fact's `updated` date, with the exact lines (anchors) that cite it.
|
|
33
|
+
3. For each stale artifact, draft the minimal edit that makes the anchored line
|
|
34
|
+
true again. Rules:
|
|
35
|
+
- Change the sentence, keep the voice of that artifact (a manual is not a deck).
|
|
36
|
+
- Keep the anchor comment on the line. The drift check needs the new `value`
|
|
37
|
+
to appear on that line.
|
|
38
|
+
- Code: change the constant, not the comment. Derived values (enums, ranges,
|
|
39
|
+
schemas) move with it.
|
|
40
|
+
- `lifecycle: snapshot` artifacts are frozen. Never edit them; they are history.
|
|
41
|
+
- A changelog is living: add a dated entry on top, never rewrite old entries.
|
|
42
|
+
When an old entry's fact changes, turn its anchor into an as-of citation
|
|
43
|
+
(`fact:id@<that entry's date>`) so the line keeps its link and stops being
|
|
44
|
+
checked for the current value. Same for any "was X, now Y" aside.
|
|
45
|
+
- If an edit needs a decision (new copy angle, a number that no longer fits the
|
|
46
|
+
story), stop and ask; do not invent.
|
|
47
|
+
4. Present the edit list (file, line, before → after) and apply it. If the user
|
|
48
|
+
asked for autonomous propagation, apply without asking.
|
|
49
|
+
5. Stamp the reviewed artifacts: `npx fakto touch <file>…`. If `fakto.config.json`
|
|
50
|
+
declares `renders`, run `npx fakto render` next: generated outputs (a slide deck
|
|
51
|
+
built from a Markdown deck) are rebuilt from their source, never edited by hand.
|
|
52
|
+
6. Run `npx fakto check`. It must print `0 errors` with no stale warnings. Fix anything
|
|
53
|
+
it reports before you call the job done.
|
|
54
|
+
|
|
55
|
+
### B. Retiring a fact
|
|
56
|
+
|
|
57
|
+
1. Set `status: retired` on the record, bump `updated`, add a `note:` saying why.
|
|
58
|
+
2. Run `npx fakto check`. Every living artifact that cites it is now an error.
|
|
59
|
+
3. Remove or rewrite each citing line. Where the artifact should record that the
|
|
60
|
+
thing existed (a changelog, a history section), keep the anchor as an as-of
|
|
61
|
+
citation (`fact:id@date`) or put `RETIRED` on the line.
|
|
62
|
+
4. `npx fakto touch` the edited files, `npx fakto check` must be green.
|
|
63
|
+
|
|
64
|
+
### C. Adding a new fact
|
|
65
|
+
|
|
66
|
+
1. One record per fact, kebab-case id with a topic prefix, `status: live` only
|
|
67
|
+
when the value is confirmed; otherwise `draft`.
|
|
68
|
+
2. `value` is the shortest string every artifact line must contain. Pick it so
|
|
69
|
+
it matches prose and code alike (`"7"`, `"$4.99"`, `"client.send("`).
|
|
70
|
+
3. Anchor the artifacts that already state it. Run `npx fakto graph` to confirm it has
|
|
71
|
+
dependents; a fact with none is either new copy to write or not a fact.
|
|
72
|
+
|
|
73
|
+
### D. Tagging a new artifact
|
|
74
|
+
|
|
75
|
+
1. Add `reviewed: YYYY-MM-DD` to the file head (frontmatter or a comment).
|
|
76
|
+
2. Anchor every line that states a fact: `<!-- fact:id -->`, `// fact:id`,
|
|
77
|
+
`# fact:id`, whatever comment syntax the file uses.
|
|
78
|
+
3. A published, dated thing (a post, a press release, an old deck) gets
|
|
79
|
+
`lifecycle: snapshot` and is never edited again.
|
|
80
|
+
4. `npx fakto check` must be green.
|
|
81
|
+
|
|
82
|
+
### E. Narratives (facts without a `value`)
|
|
83
|
+
|
|
84
|
+
A narrative is a story every channel tells in its own words ("a platform, not an
|
|
85
|
+
appliance"). Its record has a `statement` and no `value`, and usually `supports: [ids]`,
|
|
86
|
+
the facts it rests on. The check cannot compare wording, so you are the check:
|
|
87
|
+
|
|
88
|
+
1. When a narrative's statement changes, `fakto stale` lists every section anchored to
|
|
89
|
+
it. Rewrite each so it tells the new story in that file's voice: a website headline,
|
|
90
|
+
a slide, a sentence in a deck. Do not paste the statement verbatim unless the file
|
|
91
|
+
is meant to quote it.
|
|
92
|
+
2. Consistency pass: whenever you rewrite a file that cites a narrative (for any
|
|
93
|
+
reason), read the anchored section and confirm it still tells that narrative. If it
|
|
94
|
+
no longer does, rewrite it or report it; never leave a section that contradicts its
|
|
95
|
+
story.
|
|
96
|
+
3. Numbers inside a narrative section are still facts: anchor them separately.
|
|
97
|
+
4. `supports` must list existing fact ids; the check fails otherwise.
|
|
98
|
+
|
|
99
|
+
### F. A source file changed (sync)
|
|
100
|
+
|
|
101
|
+
Some facts and sections follow a file instead of the other way round: a fact with
|
|
102
|
+
`source:`, a section with `<!-- from:path#selector -->`, a fence after
|
|
103
|
+
`<!-- example:path#selector -->`. When that file changes:
|
|
104
|
+
|
|
105
|
+
1. Run `npx fakto sync`. It copies every fenced example from its source (never edit those
|
|
106
|
+
by hand) and lists the facts and sections that need judgment, with the diff since their
|
|
107
|
+
last sync where git has the old version.
|
|
108
|
+
2. **Facts.** For each listed fact, read the source (the anchored line, or the selection).
|
|
109
|
+
Change `value` and `statement` only if the meaning changed, and bump `updated` when you
|
|
110
|
+
do. For a source with a selector, then run `npx fakto touch <fact-id>` to restamp it,
|
|
111
|
+
also when nothing changed. A derived fact is never edited for any other reason.
|
|
112
|
+
3. **Sections.** For each listed section, reread it against the source. Rewrite it only
|
|
113
|
+
where it no longer describes the source, in that file's own voice, keeping its anchors.
|
|
114
|
+
Then `npx fakto touch <file>`: it stamps `reviewed` and moves the `from:` hashes.
|
|
115
|
+
4. **Propagate.** If step 2 changed a fact, follow case A from step 2 (`npx fakto stale`,
|
|
116
|
+
rewrite, touch, render).
|
|
117
|
+
5. `npx fakto check` must be green. A rewrite can change a file another section draws on;
|
|
118
|
+
if `fakto check` still reports something behind its source, go again (at most three
|
|
119
|
+
passes, then report what is left).
|
|
120
|
+
|
|
121
|
+
## Invariants
|
|
122
|
+
|
|
123
|
+
- No number, name or claim in an artifact without a fact record and an anchor.
|
|
124
|
+
- Forward links only: artifacts point at facts. The reverse direction is derived
|
|
125
|
+
by `npx fakto graph`, never stored.
|
|
126
|
+
- The `updated` date on a fact moves on every value change. That date is the
|
|
127
|
+
whole propagation mechanism; forget it and nothing goes stale.
|
|
128
|
+
- Snapshots are never edited. Living artifacts are never left stale.
|
|
129
|
+
- Render outputs are build output: edit the source, then `npx fakto render`.
|
|
130
|
+
- A derived fact (one with `source:`) follows its file: change the file, never the record.
|
|
131
|
+
- Fenced examples are copies: `npx fakto sync` writes them, nobody else.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: fakto-canvas
|
|
3
|
+
description: >
|
|
4
|
+
Start the Fakto canvas, the live map of this repo's facts and the files that state them,
|
|
5
|
+
and tell the user where it runs. Use it first, at the start of every session in a repo
|
|
6
|
+
that has a fakto.config.json, before any other work. Also use it whenever the user wants
|
|
7
|
+
to see the map, the canvas, or the changes as they happen, or says "start", "run it",
|
|
8
|
+
"open the canvas" or "show me".
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# fakto-canvas — start the live map and say where it is
|
|
12
|
+
|
|
13
|
+
The canvas is a local web page that draws every fact and every file that cites it, and
|
|
14
|
+
updates live as files change. The user watches it while you work in the terminal, so
|
|
15
|
+
start it before anything else.
|
|
16
|
+
|
|
17
|
+
1. Run `npx fakto canvas --background`. It starts the canvas detached (or finds the one already
|
|
18
|
+
running for this repo) and returns within a few seconds with one line:
|
|
19
|
+
- `fakto canvas: http://localhost:<port> (started in the background …)`: it started.
|
|
20
|
+
- `fakto canvas: http://localhost:<port> (already running for …)`: one was already up.
|
|
21
|
+
- `The canvas is not installed` or `Cannot find package`: the dependencies are missing.
|
|
22
|
+
Run `npm install`, then try again.
|
|
23
|
+
Never run plain `npx fakto canvas` yourself: it stays in the foreground and blocks you.
|
|
24
|
+
2. If it timed out, read the log file it names and tell the user what it says.
|
|
25
|
+
3. Run `npx fakto check` and keep its last line (the error and warning count).
|
|
26
|
+
4. Tell the user, in two short lines, with the real URL and counts:
|
|
27
|
+
> The Fakto canvas is running at http://localhost:4848. Open it to watch facts and files
|
|
28
|
+
> update as we work.
|
|
29
|
+
> Right now: 31 facts, 15 files, check green (or: what the check reports).
|
|
30
|
+
|
|
31
|
+
Get the counts from `npx fakto graph --json` if you need them (`facts.length`,
|
|
32
|
+
`artifacts.length`).
|
|
33
|
+
|
|
34
|
+
## While it runs
|
|
35
|
+
|
|
36
|
+
- Every edit you make shows up on the canvas within a second; mention it when the user should
|
|
37
|
+
look ("watch the marketing column").
|
|
38
|
+
- The canvas has its own buttons (Save on a fact, Sync, Sync all). They run the user's Claude
|
|
39
|
+
Code CLI in this repo; their results land in the same files you see.
|
|
40
|
+
- Leave it running when the session ends. If the user asks to stop it: `npx fakto canvas --stop`.
|