faf-cli 7.12.1 → 7.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/README.md +39 -32
- package/dist/cli.js +364 -337
- package/dist/cli.js.map +70 -59
- package/dist/core/cwd-guard.d.ts +8 -0
- package/dist/core/faf-dna.d.ts +147 -0
- package/dist/core/faf-source.d.ts +27 -0
- package/dist/core/json-edit.d.ts +77 -0
- package/dist/core/render-hash.d.ts +55 -0
- package/dist/core/safe-write.d.ts +228 -0
- package/dist/core/scorer.d.ts +7 -2
- package/dist/core/shape.d.ts +11 -0
- package/dist/core/slots.d.ts +30 -0
- package/dist/core/typed-none.d.ts +25 -0
- package/dist/core/types.d.ts +8 -0
- package/dist/core/yaml-edit.d.ts +136 -0
- package/dist/detect/assemble.d.ts +49 -5
- package/dist/detect/git-repo.d.ts +50 -0
- package/dist/detect/scanner.d.ts +9 -4
- package/dist/detect/stack.d.ts +14 -1
- package/dist/detect/turbo-cat-knowledge.d.ts +10 -0
- package/dist/detect/turbo-cat.d.ts +6 -2
- package/dist/fafm/index.d.ts +1 -0
- package/dist/fafm/soul.d.ts +108 -6
- package/dist/index.d.ts +24 -3
- package/dist/index.js +203 -163
- package/dist/index.js.map +35 -23
- package/dist/interop/cards.d.ts +52 -3
- package/dist/interop/claude-memory.d.ts +134 -0
- package/dist/interop/claude.d.ts +5 -1
- package/dist/interop/commonmark.d.ts +144 -0
- package/dist/interop/copilot-instructions.d.ts +1 -0
- package/dist/interop/faf.d.ts +123 -11
- package/dist/interop/inject.d.ts +112 -22
- package/dist/interop/projecthtml.d.ts +19 -2
- package/dist/interop/servercard.d.ts +51 -2
- package/dist/wasm/kernel.d.ts +7 -4
- package/package.json +5 -4
- package/project.faf +11 -4
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** True if dir is the user's home directory or the filesystem root — compared
|
|
2
|
+
* by identity (device + inode), not by spelling. */
|
|
3
|
+
export declare function isNonProjectRoot(dir?: string): boolean;
|
|
4
|
+
/**
|
|
5
|
+
* Exit 1 with a clear cd instruction if cwd cannot be a project root.
|
|
6
|
+
* Call at the top of commands that create or interview project.faf.
|
|
7
|
+
*/
|
|
8
|
+
export declare function assertProjectCwd(dir?: string, command?: string): void;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FAF DNA — the lifecycle of AI context. The "first heartbeat".
|
|
3
|
+
*
|
|
4
|
+
* Every project.faf gets a `.faf-dna` lineage record (separate file, NOT
|
|
5
|
+
* embedded in the .faf — so it's compatible with the clean dialect):
|
|
6
|
+
* - Birth Certificate: the honest first score (even 0%) — the "before" picture
|
|
7
|
+
* - Growth Record: version history as the score improves
|
|
8
|
+
* - Journey: the one-line story, e.g. "22% → 85% → 99% ← 92%"
|
|
9
|
+
*
|
|
10
|
+
* Birth DNA = the raw slot-based score at init. The growth from Birth DNA to
|
|
11
|
+
* the current score is the demonstrated value of FAF.
|
|
12
|
+
*
|
|
13
|
+
* Restored 2026-05-21 — silently dropped in the v6.0 clean-architecture rewrite
|
|
14
|
+
* (it was never knowingly removed). Ported from v5 (sync, v6-native, load-compatible).
|
|
15
|
+
*
|
|
16
|
+
* Other shapes. `.faf-dna` is committed lineage, and other tools have written
|
|
17
|
+
* it in their own shape (claude-faf-mcp's faf_dna wrote `milestones` at the top
|
|
18
|
+
* level and no `versions` or `growth`). Reading never throws on such a file: it
|
|
19
|
+
* reads what it can, and missing parts are derived from what is there. Writing
|
|
20
|
+
* is different: recordGrowth rewrites the file only when faf can prove it wrote
|
|
21
|
+
* every byte — the file is in faf's shape AND its text is exactly faf's own
|
|
22
|
+
* serialisation of what it holds (`JSON.stringify(data, null, 2)` and a final
|
|
23
|
+
* newline), and nothing faf would replace carries a note of the user's. Hand
|
|
24
|
+
* formatting, CRLF, reordered keys, a repeated key or a number JSON cannot hold
|
|
25
|
+
* exactly (a 20-digit id) would all be lost in a rewrite, so such a file — like
|
|
26
|
+
* one in another shape — is read and left exactly as it is, and
|
|
27
|
+
* `readOnlyReason()` says why in one line. `faf init --force` starts a fresh
|
|
28
|
+
* lineage when you ask it to.
|
|
29
|
+
*/
|
|
30
|
+
export interface BirthCertificate {
|
|
31
|
+
born: string;
|
|
32
|
+
birthDNA: number;
|
|
33
|
+
birthDNASource: 'init' | 'legacy';
|
|
34
|
+
projectDNA: string;
|
|
35
|
+
certificate: string;
|
|
36
|
+
}
|
|
37
|
+
export interface VersionEntry {
|
|
38
|
+
version: string;
|
|
39
|
+
timestamp: string;
|
|
40
|
+
score: number;
|
|
41
|
+
changes: string[];
|
|
42
|
+
growth: number;
|
|
43
|
+
}
|
|
44
|
+
/** Journey markers only — not score tiers.
|
|
45
|
+
* Tiers live solely in tiers.ts (Trophy 100 · Gold 99 · Silver 95 · Bronze 85 · …).
|
|
46
|
+
* There is no championship / elite / perfect milestone. */
|
|
47
|
+
export interface Milestone {
|
|
48
|
+
type: 'birth' | 'doubled' | 'peak' | 'current';
|
|
49
|
+
score: number;
|
|
50
|
+
date: string;
|
|
51
|
+
version: string;
|
|
52
|
+
label: string;
|
|
53
|
+
emoji: string;
|
|
54
|
+
}
|
|
55
|
+
export interface FafDNA {
|
|
56
|
+
birthCertificate: BirthCertificate;
|
|
57
|
+
versions: VersionEntry[];
|
|
58
|
+
current: {
|
|
59
|
+
version: string;
|
|
60
|
+
score: number;
|
|
61
|
+
lastSync: string;
|
|
62
|
+
};
|
|
63
|
+
growth: {
|
|
64
|
+
totalGrowth: number;
|
|
65
|
+
daysActive: number;
|
|
66
|
+
milestones: Milestone[];
|
|
67
|
+
};
|
|
68
|
+
lastModified: string;
|
|
69
|
+
format: 'faf-dna-v1';
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The `.faf-dna` lineage of one project: birth, growth and the journey line.
|
|
73
|
+
* This is the API `faf init` (birth), `faf auto` / `faf refresh` (recordGrowth)
|
|
74
|
+
* and `faf dna` (getJourney, getBirthDNADisplay, getLog) use.
|
|
75
|
+
*
|
|
76
|
+
* - `birth(score)` starts a new lineage and writes it, replacing any
|
|
77
|
+
* `.faf-dna` there (check `exists()` first unless a fresh start is meant —
|
|
78
|
+
* `faf init` births only when there is none, or with `--force`).
|
|
79
|
+
* - `recordGrowth(score, changes)` adds a version when the score changed.
|
|
80
|
+
* It adds only to a file faf can prove it wrote (see the file header);
|
|
81
|
+
* for any other file it writes nothing and returns null, and
|
|
82
|
+
* `readOnlyReason()` says why.
|
|
83
|
+
* - Reads never throw: a missing, unreadable, non-UTF-8 or non-JSON file,
|
|
84
|
+
* or one with no usable birth certificate, reads as null / '' / [].
|
|
85
|
+
*
|
|
86
|
+
* Every write is atomic and stays inside the project: a `.faf-dna` link that
|
|
87
|
+
* leads out of the project, dangles or leads to a file with another name is
|
|
88
|
+
* refused (and reads as absent). A write is refused, too, when the file
|
|
89
|
+
* changed on disk after this manager read it (another `faf` process recorded
|
|
90
|
+
* growth meanwhile, say): SafePathError `changed`, nothing written.
|
|
91
|
+
*/
|
|
92
|
+
export declare class FafDNAManager {
|
|
93
|
+
private readonly projectPath;
|
|
94
|
+
private readonly dnaPath;
|
|
95
|
+
private dna;
|
|
96
|
+
/** True when the loaded file is one faf wrote (so it may be added to). */
|
|
97
|
+
private own;
|
|
98
|
+
/** The text read from `.faf-dna` (or last written), for the write's check. */
|
|
99
|
+
private text;
|
|
100
|
+
/** Why the file is read only, when it is; null when faf may add to it. */
|
|
101
|
+
private reason;
|
|
102
|
+
/** True once this manager found no `.faf-dna` (exists / load / birth) and
|
|
103
|
+
* has not written one since: a birth then refuses a file that appeared
|
|
104
|
+
* meanwhile instead of writing over it. */
|
|
105
|
+
private sawNoFile;
|
|
106
|
+
constructor(projectPath: string);
|
|
107
|
+
exists(): boolean;
|
|
108
|
+
/** Birth — the first heartbeat. Writes the birth certificate with the honest first score.
|
|
109
|
+
* When this manager found no `.faf-dna` (now, or at an earlier exists() /
|
|
110
|
+
* load()), a file that appeared since is refused (SafePathError `changed`)
|
|
111
|
+
* rather than written over; over a `.faf-dna` that is there, birth starts a
|
|
112
|
+
* fresh lineage (`faf init --force`). */
|
|
113
|
+
birth(birthDNA: number): FafDNA;
|
|
114
|
+
/** True when `.faf-dna` exists and is one faf wrote — in faf's shape, and
|
|
115
|
+
* exactly faf's own text — so recordGrowth may add to it. Any other file is
|
|
116
|
+
* read, not written. */
|
|
117
|
+
isFafShape(): boolean;
|
|
118
|
+
/** Why faf leaves this `.faf-dna` as it is, in one plain line — or null when
|
|
119
|
+
* there is none, or recordGrowth may add to it. */
|
|
120
|
+
readOnlyReason(): string | null;
|
|
121
|
+
/** Record growth — a new score on the journey. Returns null when there is no
|
|
122
|
+
* DNA yet, or when the file is in another tool's shape (nothing is written). */
|
|
123
|
+
recordGrowth(newScore: number, changes: string[]): FafDNA | null;
|
|
124
|
+
/** The one-line journey: e.g. "22% → 85% → 99% ← 92%". */
|
|
125
|
+
getJourney(): string;
|
|
126
|
+
getBirthDNADisplay(): {
|
|
127
|
+
current: number;
|
|
128
|
+
birthDNA: number;
|
|
129
|
+
growth: number;
|
|
130
|
+
born: string;
|
|
131
|
+
} | null;
|
|
132
|
+
/** Complete version history, newest last. */
|
|
133
|
+
getLog(): string[];
|
|
134
|
+
/** Load `.faf-dna`. Never throws: missing, unreadable, not UTF-8, not JSON, a
|
|
135
|
+
* refused link, or no usable birth certificate → null. A file faf did not
|
|
136
|
+
* write (another shape, or not exactly faf's text) loads as a readable view
|
|
137
|
+
* (see the file header). */
|
|
138
|
+
load(): FafDNA | null;
|
|
139
|
+
/** The file's text (strict UTF-8) and its JSON, or null with the reason noted. */
|
|
140
|
+
private read;
|
|
141
|
+
private save;
|
|
142
|
+
private generateProjectDNA;
|
|
143
|
+
private generateCertificate;
|
|
144
|
+
private incrementVersion;
|
|
145
|
+
private daysSince;
|
|
146
|
+
private updateMilestones;
|
|
147
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where .faf data came from: the file readFaf read it from (its real path) and
|
|
3
|
+
* the text it read. writeFaf checks a write of that data against it — when the
|
|
4
|
+
* file changed on disk after the read, the write is refused instead of being
|
|
5
|
+
* made over the newer edit. Kept apart from interop/faf.ts so the pure parts
|
|
6
|
+
* of detection (updateExistingFaf) can carry it without importing a module
|
|
7
|
+
* that runs git.
|
|
8
|
+
*/
|
|
9
|
+
/** The file a data object was read from, and the text read. */
|
|
10
|
+
export interface FafSource {
|
|
11
|
+
real: string;
|
|
12
|
+
text: string;
|
|
13
|
+
}
|
|
14
|
+
/** The source recorded for `data`, if any. */
|
|
15
|
+
export declare function fafSourceOf(data: unknown): FafSource | undefined;
|
|
16
|
+
/** Record that `data` holds what `source.text` (read from `source.real`) says. */
|
|
17
|
+
export declare function setFafSource(data: object, source: FafSource): void;
|
|
18
|
+
/** Let `to` — data made from `from` (a copy with empty slots filled, say) —
|
|
19
|
+
* carry the file `from` was read from. Returns `to`. */
|
|
20
|
+
export declare function carryFafSource<T extends object>(from: unknown, to: T): T;
|
|
21
|
+
/** Mark `data` as a fill of the file it was read from — what `faf auto`
|
|
22
|
+
* (updateExistingFaf) makes: detected values in the file's empty slots.
|
|
23
|
+
* writeFaf leaves a node with an anchor that an alias reads as written in
|
|
24
|
+
* such data, since filling it would change every alias. Returns `data`. */
|
|
25
|
+
export declare function markAsFill<T extends object>(data: T): T;
|
|
26
|
+
/** True when `data` was marked with {@link markAsFill}. */
|
|
27
|
+
export declare function isFill(data: unknown): boolean;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text-preserving JSON edits: change the value of a key, or add a key that is
|
|
3
|
+
* missing, and keep every other byte of the file — key order, spacing, a
|
|
4
|
+
* 20-digit integer, an array written on one line. faf uses it for the one
|
|
5
|
+
* JSON file it shares with its owner, a registry `server.json`, where faf owns
|
|
6
|
+
* only the identity keys (`faf server-card`, `faf cards --target registry`).
|
|
7
|
+
*
|
|
8
|
+
* A patch is a plain object. For each key: a plain-object value is a folder to
|
|
9
|
+
* go into (the file's value must be an object too); any other value replaces
|
|
10
|
+
* the value's text when it differs, or is added when the key is missing.
|
|
11
|
+
* Nothing is ever deleted, and `undefined` is skipped. When the file cannot be
|
|
12
|
+
* edited that way — not valid JSON, not an object, a key it repeats on the way,
|
|
13
|
+
* a value faf would have to replace to go into it, or an object or array in the
|
|
14
|
+
* file where faf sets a plain value — a JsonEditError says why in one line and
|
|
15
|
+
* nothing is changed.
|
|
16
|
+
*
|
|
17
|
+
* Two more edits keep the same promise: {@link upsertJsonRows} updates or
|
|
18
|
+
* appends faf's own rows in an array (the `faf cards` catalog), and
|
|
19
|
+
* {@link removeJsonKey} takes out one key faf added (its render hash).
|
|
20
|
+
*/
|
|
21
|
+
/** Why a JSON text cannot be edited in place (one line). */
|
|
22
|
+
export declare class JsonEditError extends Error {
|
|
23
|
+
constructor(message: string);
|
|
24
|
+
}
|
|
25
|
+
/** Where to add a missing key: after the first of these keys the object has
|
|
26
|
+
* (else after its last key). Keyed by the dotted path of the object ('' is the root). */
|
|
27
|
+
export type InsertAfter = Record<string, Record<string, readonly string[]>>;
|
|
28
|
+
/**
|
|
29
|
+
* Apply `patch` to the JSON `text`, changing only the value text of the keys
|
|
30
|
+
* it names and adding the keys it has that the text lacks (see the file
|
|
31
|
+
* header). Returns the new text and whether it changed. Throws a
|
|
32
|
+
* JsonEditError, having changed nothing, when the text cannot be edited that
|
|
33
|
+
* way. `after` says where a missing key goes (default: after the object's last key).
|
|
34
|
+
*/
|
|
35
|
+
export declare function editJsonText(text: string, patch: Record<string, unknown>, after?: InsertAfter): {
|
|
36
|
+
text: string;
|
|
37
|
+
changed: boolean;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Update or add rows in the array at the root key `key`, changing nothing
|
|
41
|
+
* else: a row whose `id` value is exactly a row's in the file is updated in
|
|
42
|
+
* place — only the values of its `update` keys change (or are added) — and any
|
|
43
|
+
* other row is appended after the file's last item, laid out like the items
|
|
44
|
+
* around it (the array, or the key, is added when missing). Every item faf
|
|
45
|
+
* does not update stays byte for byte. Never matched by anything but `id`.
|
|
46
|
+
* Throws a JsonEditError, having changed nothing, when the text cannot be
|
|
47
|
+
* edited that way: not a JSON object, `key` not an array or there more than
|
|
48
|
+
* once, a row's id there more than once, or a value faf sets that is an object
|
|
49
|
+
* or array in the file.
|
|
50
|
+
*/
|
|
51
|
+
export declare function upsertJsonRows(text: string, key: string, rows: readonly Record<string, unknown>[], opts: {
|
|
52
|
+
id: string;
|
|
53
|
+
update: readonly string[];
|
|
54
|
+
}): {
|
|
55
|
+
text: string;
|
|
56
|
+
changed: boolean;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* The value at `path` in the JSON object `text` (each step a key of an
|
|
60
|
+
* object): `count` 1 with its parsed `value`, 0 when a step is missing or not
|
|
61
|
+
* an object, more than 1 when a key on the way is repeated. Throws a
|
|
62
|
+
* JsonEditError when `text` is not a JSON object.
|
|
63
|
+
*/
|
|
64
|
+
export declare function locateJsonKey(text: string, path: readonly string[]): {
|
|
65
|
+
count: number;
|
|
66
|
+
value?: unknown;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* `text` with the key at `path` taken out — the reverse of {@link editJsonText}
|
|
70
|
+
* adding it (see {@link removal}); every other byte stays. With
|
|
71
|
+
* `dropEmptyParent`, when that key is its object's only key, the object's own
|
|
72
|
+
* key is taken out instead. Returns null when the key is not there exactly
|
|
73
|
+
* once; throws a JsonEditError when `text` is not a JSON object.
|
|
74
|
+
*/
|
|
75
|
+
export declare function removeJsonKey(text: string, path: readonly string[], opts?: {
|
|
76
|
+
dropEmptyParent?: boolean;
|
|
77
|
+
}): string | null;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** The `_meta` key a JSON file faf renders carries its render hash under. */
|
|
2
|
+
export declare const RENDER_KEY = "one.faf/render";
|
|
3
|
+
/** `sha256:<hex>` of `text` (UTF-8). */
|
|
4
|
+
export declare function renderHash(text: string): string;
|
|
5
|
+
/** How faf records its hash in a file: an HTML meta line, or a JSON `_meta` key. */
|
|
6
|
+
export type RenderFormat = 'html' | 'json';
|
|
7
|
+
/**
|
|
8
|
+
* Whether the bytes of a whole file faf renders are exactly what faf wrote:
|
|
9
|
+
* `match` — they carry faf's render hash, the hash is that of the rest of the
|
|
10
|
+
* file, and the file is laid out exactly as faf writes it; `mismatch` — they
|
|
11
|
+
* carry a render hash that no longer fits, or (project.html) a render line
|
|
12
|
+
* that is not exactly faf's form (the file was edited since faf wrote it);
|
|
13
|
+
* `none` — they carry no render hash (a file from before 7.13, or one faf
|
|
14
|
+
* never wrote). The bytes are taken as they are: see {@link writeRendered}
|
|
15
|
+
* for a file git checked out with CRLF line ends.
|
|
16
|
+
*/
|
|
17
|
+
export declare function renderOwnership(bytes: Uint8Array, format: RenderFormat): 'match' | 'mismatch' | 'none';
|
|
18
|
+
/** Options for {@link writeRendered}. */
|
|
19
|
+
export interface RenderedWriteOptions {
|
|
20
|
+
/** The project folder the write must stay inside. */
|
|
21
|
+
root: string;
|
|
22
|
+
/** How the hash is recorded: `html` (project.html) or `json` (the cards, server.json, a snapshot). */
|
|
23
|
+
format: RenderFormat;
|
|
24
|
+
/** True when the bytes carry faf's older mark — a file faf wrote before render hashes. */
|
|
25
|
+
hasMark: (bytes: Buffer) => boolean;
|
|
26
|
+
/** That mark in words, for the refusal of a file without it. */
|
|
27
|
+
mark: string;
|
|
28
|
+
/** Replace the file whatever it holds — the explicit overwrite (`--force`). */
|
|
29
|
+
force?: boolean;
|
|
30
|
+
}
|
|
31
|
+
/** What {@link writeRendered} did. */
|
|
32
|
+
export type RenderedResult = 'created' | 'updated' | 'unchanged';
|
|
33
|
+
/**
|
|
34
|
+
* Write the whole file faf renders at `path`: `render` (faf's text, without a
|
|
35
|
+
* render hash) with faf's render hash added. The link rules and the atomic,
|
|
36
|
+
* compare-before-rename write are safe-write's. A file already there is
|
|
37
|
+
* replaced only when {@link renderOwnership} says `match`; with a `mismatch`,
|
|
38
|
+
* or with no hash, it is refused (SafePathError `not-owned`) and left byte for
|
|
39
|
+
* byte:
|
|
40
|
+
* - "<file> was edited since faf wrote it — faf left it unchanged. Use --force to replace it."
|
|
41
|
+
* - with faf's older mark (written before 7.13): exactly faf's `render` →
|
|
42
|
+
* nothing to change (`unchanged`); otherwise "<file> has no faf render hash
|
|
43
|
+
* (written before 7.13), so faf cannot tell whether it was edited — faf
|
|
44
|
+
* left it unchanged. Use --force once to replace it; after that faf
|
|
45
|
+
* recognises its own output."
|
|
46
|
+
* - without the mark: "<file> has no <mark>, so faf did not write it — faf left it unchanged."
|
|
47
|
+
* `force` replaces it anyway. A file already holding exactly the new bytes is
|
|
48
|
+
* not written again. A file whose every line ends CRLF (git's
|
|
49
|
+
* `core.autocrlf`) is judged with those line ends read as LF, and is written
|
|
50
|
+
* back with CRLF line ends.
|
|
51
|
+
*/
|
|
52
|
+
export declare function writeRendered(path: string, render: string, opts: RenderedWriteOptions): {
|
|
53
|
+
path: string;
|
|
54
|
+
result: RenderedResult;
|
|
55
|
+
};
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/** Why a path was refused. */
|
|
2
|
+
export type SafePathReason = 'outside' | 'dangling' | 'not-a-file' | 'not-faf' | 'other-file' | 'git' | 'not-utf8' | 'changed' | 'not-owned' | 'not-yaml' | 'unplaceable';
|
|
3
|
+
/**
|
|
4
|
+
* A path faf will not read or write, or a file faf will not change. Nothing
|
|
5
|
+
* was written; the file on disk is exactly as it was.
|
|
6
|
+
* - `outside`, `dangling`, `not-a-file`, `not-faf`, `other-file`, `git`: the
|
|
7
|
+
* path is refused (see {@link resolveInside}).
|
|
8
|
+
* - `not-utf8`: the file is not UTF-8 (see {@link readUtf8}).
|
|
9
|
+
* - `changed`: the file changed on disk after faf read it — its bytes, or its
|
|
10
|
+
* mode (see the `expect` option of {@link safeWriteFile}).
|
|
11
|
+
* - `not-owned`: a whole file faf renders is already there and faf cannot
|
|
12
|
+
* prove it wrote every byte of it — it has no faf mark, or it was edited
|
|
13
|
+
* since faf wrote it, or it is from before faf recorded a render hash (see
|
|
14
|
+
* {@link safeReplaceOwned} and render-hash.ts).
|
|
15
|
+
* - `not-yaml`: a .faf faf reads or edits is not valid YAML: "<file> is not
|
|
16
|
+
* valid YAML (<reason>, line N) — faf left it unchanged"; or it parses to
|
|
17
|
+
* a scalar or a list, not a mapping; or faf's scoring kernel cannot read
|
|
18
|
+
* it (`faf score`, `faf compile`, `faf refresh`).
|
|
19
|
+
* - `unplaceable`: faf could not place its managed block where its next run
|
|
20
|
+
* finds it again, so it wrote nothing (see inject.ts).
|
|
21
|
+
*
|
|
22
|
+
* `onWrite` is true when faf refused at the write itself — it may have read
|
|
23
|
+
* the file before (a `.faf` read through a link, say) — and false when it
|
|
24
|
+
* refused before reading or writing anything.
|
|
25
|
+
*/
|
|
26
|
+
export declare class SafePathError extends Error {
|
|
27
|
+
readonly reason: SafePathReason;
|
|
28
|
+
/** The path as the caller named it (absolute). */
|
|
29
|
+
readonly path: string;
|
|
30
|
+
/** True when the refusal came at the write (faf may have read the file first). */
|
|
31
|
+
readonly onWrite: boolean;
|
|
32
|
+
constructor(reason: SafePathReason, path: string, message: string, opts?: {
|
|
33
|
+
onWrite?: boolean;
|
|
34
|
+
cause?: unknown;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
export interface ResolveInsideOptions {
|
|
38
|
+
/** Resolving for a read of project context: a link must end at a `.faf` or
|
|
39
|
+
* `.fafm` file — or, when the name is an AI context file (CLAUDE.md), at
|
|
40
|
+
* another AI context file (CLAUDE.md → AGENTS.md), the rule the writers
|
|
41
|
+
* use. A plain file is read under the name the caller gave. */
|
|
42
|
+
read?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Resolving faf's own git hook (`faf hooks`) — one of the two exceptions
|
|
45
|
+
* to the `.git` rule. `dir` must be the repo's hooks folder as git names it
|
|
46
|
+
* (`git rev-parse --git-path hooks`, a folder named `hooks`), and `name` a
|
|
47
|
+
* hook file sitting directly in it (`pre-commit`). Only that file may be
|
|
48
|
+
* inside `.git`; nothing below or beside it. Links are still checked: a
|
|
49
|
+
* link must stay inside the hooks folder's real folder and end at a file of
|
|
50
|
+
* the same name.
|
|
51
|
+
*/
|
|
52
|
+
allowGitHooks?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Resolving the repo's own git config file (`faf diff --uninstall-driver`)
|
|
55
|
+
* — the other exception to the `.git` rule. `dir` must be the folder git
|
|
56
|
+
* names for it (the folder of `git rev-parse --git-path config`), and `name`
|
|
57
|
+
* the file `config` directly in it. Only that file may be inside `.git`;
|
|
58
|
+
* nothing below or beside it. A link must stay inside that folder and end
|
|
59
|
+
* at a file named `config`.
|
|
60
|
+
*/
|
|
61
|
+
allowGitConfig?: boolean;
|
|
62
|
+
}
|
|
63
|
+
export interface SafeWriteOptions {
|
|
64
|
+
/** The project folder the write must stay inside. Default: the file's own folder. */
|
|
65
|
+
root?: string;
|
|
66
|
+
/**
|
|
67
|
+
* The bytes the caller read from the file. Just before the rename the file
|
|
68
|
+
* is read again; when it no longer equals `expect`, the temp file is removed
|
|
69
|
+
* and a SafePathError (`changed`) is thrown: "<file> changed on disk while
|
|
70
|
+
* faf was writing — not written; original kept". A string is compared as its
|
|
71
|
+
* UTF-8 bytes. `null` means the caller found no file there: if one has
|
|
72
|
+
* appeared, the write is refused the same way (a missing file stays
|
|
73
|
+
* missing). Omit it to write without the byte check (the mode check below
|
|
74
|
+
* still runs).
|
|
75
|
+
*/
|
|
76
|
+
expect?: string | Uint8Array | null;
|
|
77
|
+
/** Write faf's own git hook: see {@link ResolveInsideOptions.allowGitHooks}. `root` is the hooks folder. */
|
|
78
|
+
allowGitHooks?: boolean;
|
|
79
|
+
/** Write the repo's own git config file: see {@link ResolveInsideOptions.allowGitConfig}. `root` is its folder. */
|
|
80
|
+
allowGitConfig?: boolean;
|
|
81
|
+
/** Permission bits for the written file. Default: the original's (a new
|
|
82
|
+
* file gets the default create mode). */
|
|
83
|
+
mode?: number;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The files faf's block injector writes — one managed block, every other byte
|
|
87
|
+
* of the file kept. Each injector writer (writeClaudeMd, writeAgentsMd,
|
|
88
|
+
* writeGeminiMd, writeCursorrules, writeCopilotInstructions, writeMemoryMd,
|
|
89
|
+
* writeLlmsTxt, writeClaudeMemory) takes its file name from this table, so
|
|
90
|
+
* the set is exactly the files those writers target. A link from one of these
|
|
91
|
+
* names to another (CLAUDE.md → AGENTS.md) may be followed.
|
|
92
|
+
*/
|
|
93
|
+
export declare const FAF_CONTEXT_FILES: Readonly<{
|
|
94
|
+
readonly claude: "CLAUDE.md";
|
|
95
|
+
readonly agents: "AGENTS.md";
|
|
96
|
+
readonly gemini: "GEMINI.md";
|
|
97
|
+
readonly cursorrules: ".cursorrules";
|
|
98
|
+
readonly copilot: "copilot-instructions.md";
|
|
99
|
+
readonly memory: "MEMORY.md";
|
|
100
|
+
readonly llms: "llms.txt";
|
|
101
|
+
}>;
|
|
102
|
+
/**
|
|
103
|
+
* Resolve `name` inside the project folder `dir` and return the real path to
|
|
104
|
+
* read or write — or throw a SafePathError.
|
|
105
|
+
*
|
|
106
|
+
* - the folder the file sits in must resolve to `dir` or below it
|
|
107
|
+
* - a path whose real form runs through `.git` → refused, always (the two
|
|
108
|
+
* exceptions: `allowGitHooks`, a hook file directly in the hooks folder,
|
|
109
|
+
* and `allowGitConfig`, the file `config` directly in its git folder)
|
|
110
|
+
* - a file that does not exist yet → its path inside the project
|
|
111
|
+
* - a regular file → its path (spelled as on disk)
|
|
112
|
+
* - a link → the file it points at, when that exists, is a regular file, is
|
|
113
|
+
* inside the project and out of `.git`, and has the link's own name (or
|
|
114
|
+
* both names are AI context files: CLAUDE.md → AGENTS.md). With `read`, the
|
|
115
|
+
* file must be a .faf/.fafm file instead (`project.faf → config/team.faf`),
|
|
116
|
+
* or — for an AI context file — another AI context file.
|
|
117
|
+
* - anything else (a link out, a dangling link, a link to a file with
|
|
118
|
+
* another name, a folder, a device) → refused
|
|
119
|
+
*
|
|
120
|
+
* `name` may be relative to `dir` or absolute. `dir` and the folder `name` sits
|
|
121
|
+
* in must exist (their ENOENT is thrown as is).
|
|
122
|
+
*/
|
|
123
|
+
export declare function resolveInside(dir: string, name: string, opts?: ResolveInsideOptions): string;
|
|
124
|
+
/**
|
|
125
|
+
* Read a text file faf may write back — strictly as UTF-8. A UTF-8 BOM is kept
|
|
126
|
+
* (U+FEFF at the start of the text, as a plain read gives it). A UTF-16 BOM
|
|
127
|
+
* (FF FE or FE FF) or any byte sequence that is not UTF-8 is refused with a
|
|
128
|
+
* SafePathError (`not-utf8`): "<file> is not UTF-8 — faf left it unchanged". A
|
|
129
|
+
* lenient read would turn those bytes into U+FFFD, and the next write would
|
|
130
|
+
* lose them. Other read errors (ENOENT, EACCES, …) are thrown as they are.
|
|
131
|
+
* `path` is read as given: resolve it with {@link resolveInside} first.
|
|
132
|
+
*/
|
|
133
|
+
export declare function readUtf8(path: string): string;
|
|
134
|
+
/** The bytes at `path`, or null when nothing is there (only ENOENT). */
|
|
135
|
+
export declare function readBytesIfPresent(path: string): Buffer | null;
|
|
136
|
+
/**
|
|
137
|
+
* A write that failed partway — a full disk, a quota, a read-only file, a
|
|
138
|
+
* killed rename — with the file on disk exactly as it was: "<file>: not
|
|
139
|
+
* written; original kept (<why>)" ("not written" alone when there was no file).
|
|
140
|
+
* `cause` is the error underneath. The CLI prints it as one line.
|
|
141
|
+
*/
|
|
142
|
+
export declare class NotWrittenError extends Error {
|
|
143
|
+
/** The file that was not written. */
|
|
144
|
+
readonly path: string;
|
|
145
|
+
constructor(path: string, message: string, opts?: {
|
|
146
|
+
cause?: unknown;
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Write a file inside a project, safely: resolved with {@link resolveInside}
|
|
151
|
+
* (a link that leaves the project, dangles, or leads to a file with another
|
|
152
|
+
* name, and anything in `.git`, is refused), then replaced atomically (temp
|
|
153
|
+
* file in the same folder, fsync, rename; the original's permissions — and,
|
|
154
|
+
* where the OS allows, its owner — kept). On any failure the original is
|
|
155
|
+
* untouched and the Error says "not written; original kept". With `expect`,
|
|
156
|
+
* a file that changed after the caller read it is not replaced either
|
|
157
|
+
* (SafePathError `changed`); nor is one whose mode changed, or that became
|
|
158
|
+
* read-only, while faf was writing. Returns the real path written — the
|
|
159
|
+
* link's target when `path` is an in-project link.
|
|
160
|
+
*/
|
|
161
|
+
export declare function safeWriteFile(path: string, content: string | Uint8Array, opts?: SafeWriteOptions): string;
|
|
162
|
+
/** Options for {@link safeReplaceOwned}. */
|
|
163
|
+
export interface ReplaceOwnedOptions {
|
|
164
|
+
/** The project folder the write must stay inside. Default: the file's own folder. */
|
|
165
|
+
root?: string;
|
|
166
|
+
/** True when the bytes already at the path carry faf's own mark (faf wrote them). */
|
|
167
|
+
owns: (existing: Buffer) => boolean;
|
|
168
|
+
/** faf's mark in words, for the refusal: "the `_meta[\"one.faf/context\"]` block". */
|
|
169
|
+
mark: string;
|
|
170
|
+
/** Replace a file without the mark anyway — the explicit overwrite (`--force`). */
|
|
171
|
+
force?: boolean;
|
|
172
|
+
/** The bytes the caller read there earlier (`null`: no file then). When
|
|
173
|
+
* given, the file must still hold them, else SafePathError `changed`. */
|
|
174
|
+
expect?: string | Uint8Array | null;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Write a whole file faf renders (project.html, a Server Card, an A2A card, a
|
|
178
|
+
* `.fafb`), replacing a file already at `path` only when faf can prove it wrote
|
|
179
|
+
* it: its bytes carry faf's own mark (`owns`). A file without the mark is
|
|
180
|
+
* refused — SafePathError `not-owned`: "<file> has no <mark>, so faf did not
|
|
181
|
+
* write it — faf left it unchanged." — and stays byte for byte, unless `force`
|
|
182
|
+
* asks to replace it. The link rules and the atomic write are
|
|
183
|
+
* {@link safeWriteFile}'s, and the write is refused if the file changed on
|
|
184
|
+
* disk after faf read it. Returns the real path written.
|
|
185
|
+
*/
|
|
186
|
+
export declare function safeReplaceOwned(path: string, content: string | Uint8Array, opts: ReplaceOwnedOptions): string;
|
|
187
|
+
/**
|
|
188
|
+
* Create the folder `dir` inside the project folder `root`, with any missing
|
|
189
|
+
* folders between them, and return its real path. `root` itself is the
|
|
190
|
+
* caller's own folder: it is created as named when missing. Below it, faf
|
|
191
|
+
* never creates a folder through a link: an existing folder on the way that
|
|
192
|
+
* is a link leading out of `root` (or a dangling link) is refused, as is any
|
|
193
|
+
* part inside `.git` and anything on the way that is not a folder — so
|
|
194
|
+
* `.github → ~/elsewhere` cannot make faf create `~/elsewhere/workflows`.
|
|
195
|
+
*/
|
|
196
|
+
export declare function makeDirInside(root: string, dir?: string): string;
|
|
197
|
+
/**
|
|
198
|
+
* Remove a file faf wrote — only when it still holds exactly `expect` (the
|
|
199
|
+
* bytes faf wrote or read there) and is a regular file inside `root` (default:
|
|
200
|
+
* its own folder), out of `.git`. A link is never removed (faf did not make
|
|
201
|
+
* it), and a file whose bytes changed is left as it is (SafePathError
|
|
202
|
+
* `changed`). Returns false when nothing was there.
|
|
203
|
+
*/
|
|
204
|
+
export declare function safeUnlink(path: string, opts: {
|
|
205
|
+
root?: string;
|
|
206
|
+
expect: string | Uint8Array;
|
|
207
|
+
}): boolean;
|
|
208
|
+
/** The marker file makeTempDir writes into every temp folder it makes, and
|
|
209
|
+
* its exact content: `faf clear` removes only a folder that carries it. */
|
|
210
|
+
export declare const TEMP_MARKER = ".faf-temp";
|
|
211
|
+
/** Make a new temp folder for faf's own use — `<os temp>/<prefix>XXXXXX`, a
|
|
212
|
+
* fresh name no one else can have made (mkdtemp) — write faf's marker file
|
|
213
|
+
* into it ({@link TEMP_MARKER}), and return its real path. Anything faf puts
|
|
214
|
+
* there (a clone, say) goes in a subfolder, beside the marker. */
|
|
215
|
+
export declare function makeTempDir(prefix: string): string;
|
|
216
|
+
/** Remove a temp folder {@link makeTempDir} made in this process, with what is
|
|
217
|
+
* in it. Any other folder is refused (an Error; nothing removed). */
|
|
218
|
+
export declare function removeTempDir(dir: string): void;
|
|
219
|
+
/**
|
|
220
|
+
* Remove faf's own temp folders left behind by earlier runs (`faf clear`):
|
|
221
|
+
* only folders faf made — entries of the OS temp folder named exactly as
|
|
222
|
+
* mkdtemp names them (`prefix` plus six letters or digits), that are real
|
|
223
|
+
* folders (a link is left alone), belong to this user and carry the marker
|
|
224
|
+
* file {@link makeTempDir} writes into each one. A folder of yours that only
|
|
225
|
+
* starts with the prefix (`faf-git-my-notes`) stays. Returns how many were
|
|
226
|
+
* removed. A folder that cannot be read or removed is skipped.
|
|
227
|
+
*/
|
|
228
|
+
export declare function removeStaleTempDirs(prefix: string): number;
|
package/dist/core/scorer.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { KernelScoreResult, ScoreResult } from './types.js';
|
|
2
|
+
/** A score as text for display: `85%`, or `unknown (—)` when the result says
|
|
3
|
+
* the score is not known (`unknown: true` — an About Repo with no
|
|
4
|
+
* source_score). Never `-1%`. */
|
|
5
|
+
export declare function scoreText(result: Pick<ScoreResult, 'score' | 'unknown'>): string;
|
|
2
6
|
/** Convert kernel result into enriched ScoreResult */
|
|
3
7
|
export declare function enrichScore(kernel: KernelScoreResult): ScoreResult;
|
|
4
8
|
/**
|
|
@@ -8,8 +12,9 @@ export declare function enrichScore(kernel: KernelScoreResult): ScoreResult;
|
|
|
8
12
|
* (`about.represents`). The scorer reads `about.source_score` and emits
|
|
9
13
|
* that directly — no slot scoring, no kernel call.
|
|
10
14
|
*
|
|
11
|
-
* Optional: `about.source_score: <number>` — without it
|
|
12
|
-
* (
|
|
15
|
+
* Optional: `about.source_score: <number>` — without it the score is not
|
|
16
|
+
* known: the result carries `unknown: true` (with score -1 and a White tier
|
|
17
|
+
* as placeholders). Render that as "unknown" (—), never as "-1/100" or "-1%".
|
|
13
18
|
*
|
|
14
19
|
* Doctrine: memory/private-source-public-about-pattern.md.
|
|
15
20
|
*
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** True for a YAML mapping: a plain object, not a list. */
|
|
2
|
+
export declare function isMapping(value: unknown): value is Record<string, unknown>;
|
|
3
|
+
/** Plain words for a value's YAML shape, for error messages: `a string ("old-init")`, `a list (2 items)`. */
|
|
4
|
+
export declare function describeShape(value: unknown): string;
|
|
5
|
+
/**
|
|
6
|
+
* A parsed .faf document as a mapping. An empty document reads as `{}`; a
|
|
7
|
+
* scalar or a list throws a SafePathError (`not-yaml`), so the CLI prints it
|
|
8
|
+
* as one line — faf never rewrites a file whose shape it does not
|
|
9
|
+
* understand. `source` names the file (or caller) in the error.
|
|
10
|
+
*/
|
|
11
|
+
export declare function asFafMapping(value: unknown, source: string): Record<string, unknown>;
|
package/dist/core/slots.d.ts
CHANGED
|
@@ -26,8 +26,38 @@ export declare const BASE_SLOTS: SlotDef[];
|
|
|
26
26
|
export declare const ENTERPRISE_SLOTS: SlotDef[];
|
|
27
27
|
/** Placeholder values treated as Empty */
|
|
28
28
|
export declare const PLACEHOLDERS: Set<string>;
|
|
29
|
+
/** `None`, `N/A`, `not applicable` (any case):
|
|
30
|
+
* a typed none — counts as an empty slot. It scores 0 until filled; faf auto
|
|
31
|
+
* fills a tech slot from a repo fact and otherwise keeps the words as typed.
|
|
32
|
+
* Only in a tech slot the app-type leaves out does faf auto write
|
|
33
|
+
* `slotignored` over them — the app-type's decision, never the words'. */
|
|
34
|
+
export declare const EXPLICIT_NONE: ReadonlySet<string>;
|
|
35
|
+
/** The value that marks a slot as not applicable (the What-Not). It comes
|
|
36
|
+
* only from the app-type (shown to people as N/A); a typed word never means it. */
|
|
37
|
+
export declare const SLOTIGNORED = "slotignored";
|
|
38
|
+
/** True for a typed none — counts as an empty slot: `None`, `N/A`,
|
|
39
|
+
* `not applicable`, `none` (any case, any padding). */
|
|
40
|
+
export declare function isExplicitNone(value: unknown): boolean;
|
|
41
|
+
/** True for words typed into a slot that do not fill it: a typed none
|
|
42
|
+
* (`None`, `N/A`, `not applicable`) or any other placeholder word
|
|
43
|
+
* (`unknown`, `null` written as a string, …), any case, any padding. Like a
|
|
44
|
+
* typed none, they count as an empty slot, and faf keeps them as typed until
|
|
45
|
+
* a repo fact fills a tech slot (or, in a tech slot the app-type leaves out,
|
|
46
|
+
* `faf auto` marks it `slotignored`). An empty value (`''`, YAML null) is
|
|
47
|
+
* not typed words. */
|
|
48
|
+
export declare function isTypedWords(value: unknown): boolean;
|
|
29
49
|
/** Check if a value is a placeholder (empty) */
|
|
30
50
|
export declare function isPlaceholder(value: unknown): boolean;
|
|
51
|
+
/** The `# found:` note of a type detection chose with no classifying signal
|
|
52
|
+
* (its `library` fallback). A type carrying it is faf's fallback, not a fact
|
|
53
|
+
* about the repo: `faf auto` writes no `slotignored` over typed words on it,
|
|
54
|
+
* and `faf score` promises none. */
|
|
55
|
+
export declare const NO_CLASSIFYING_SIGNALS = "no classifying signals \u2014 fallback";
|
|
56
|
+
/** Whether the app-type `type` uses `slot` — the one rule for which slots
|
|
57
|
+
* count: the type's active categories, less the slots a `framework` repo
|
|
58
|
+
* leaves out. `null` when `type` is not an app-type faf knows (then faf
|
|
59
|
+
* cannot tell, and decides nothing from it). */
|
|
60
|
+
export declare function appTypeUsesSlot(type: unknown, slot: SlotDef): boolean | null;
|
|
31
61
|
/** App-type to active category mapping. The canonical 24-type ladder —
|
|
32
62
|
* detectable apps + `encyclopedia` (curated knowledge) + `intent` (seed).
|
|
33
63
|
* About is NOT an app_type — it is a repo role (`about.represents`).
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ScoreResult } from './types.js';
|
|
2
|
+
/** True when the .faf text's `project.type` line carries faf's
|
|
3
|
+
* `# found: no classifying signals — fallback` note: the type is the
|
|
4
|
+
* `library` detection fell back to, not a fact, and decides no slot. */
|
|
5
|
+
export declare function typeIsFallback(text: string): boolean;
|
|
6
|
+
/** Which hint lines {@link typedNoneHints} gives. */
|
|
7
|
+
export interface TypedNoneHintOptions {
|
|
8
|
+
/** Also give the line for a slot the app-type leaves out (default true).
|
|
9
|
+
* `faf auto` passes false: by then it has marked those slots. */
|
|
10
|
+
outOfType?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* One line for each slot that `result` scores as empty and that holds typed
|
|
14
|
+
* words, by the file's app-type (`project.type`):
|
|
15
|
+
* - a slot the app-type needs:
|
|
16
|
+
* `<slot> says '<words>' — this app-type needs it, so it counts as empty until filled.`
|
|
17
|
+
* - a slot the app-type leaves out (before `faf auto` has run):
|
|
18
|
+
* `<slot> says '<words>' — this app-type doesn't use it; faf auto marks it slotignored (N/A).`
|
|
19
|
+
* A file whose type faf does not know, or whose type is the `library`
|
|
20
|
+
* detection fell back to (see {@link typeIsFallback}), gives the first line
|
|
21
|
+
* for the slots a library needs (as faf's detection reads such a file) and no
|
|
22
|
+
* second line. Reads `yaml` only; nothing is written. YAML that does not parse
|
|
23
|
+
* to a mapping gives no lines.
|
|
24
|
+
*/
|
|
25
|
+
export declare function typedNoneHints(yaml: string, result: Pick<ScoreResult, 'slots'>, opts?: TypedNoneHintOptions): string[];
|