faf-cli 7.12.0 → 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.
@@ -60,6 +60,14 @@ export interface ScoreResult {
60
60
  * `inherited: true` per schema validation.
61
61
  */
62
62
  represents?: string;
63
+ /**
64
+ * True when the score is NOT KNOWN: an About Repo with no valid
65
+ * `about.source_score`. `score` is then -1 and `tier` is White only as a
66
+ * placeholder — neither is a result. Render it as "unknown" (—), never as a
67
+ * number or a percentage ("-1/100", "-1%"), and do not seal a receipt or
68
+ * attest it. Set only when true (absent on every calculated or inherited score).
69
+ */
70
+ unknown?: boolean;
63
71
  }
64
72
  /** Tier boundary info */
65
73
  export interface TierInfo {
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Document-preserving YAML edits — how faf changes a .faf or .fafm that is
3
+ * already on disk.
4
+ *
5
+ * The file is parsed with yaml's `parseDocument`, the change is made on the
6
+ * Document through its node APIs (`set` / `setIn` / `delete`), and only the
7
+ * text of the nodes that changed is rewritten. Every other byte stays as it
8
+ * was: comments, blank lines, key order, quoting, scalar source text (`1.10`,
9
+ * `0x1F90`, a 20-digit integer), anchors and aliases, unknown keys, CRLF line
10
+ * ends and a BOM.
11
+ *
12
+ * Safety net: the spliced text is parsed again and must hold the same data as
13
+ * the changed Document and serialise the same way (comments, styles, anchors;
14
+ * blank lines aside). If it does not (a shape the splicer does not handle),
15
+ * faf writes the Document's own serialisation instead — `toString({
16
+ * lineWidth: 0 })`, which still keeps comments, anchors, unknown keys and the
17
+ * source text of every number it did not change. A change that leaves the
18
+ * data as it was returns the original text untouched — even when that
19
+ * serialisation would differ — so callers skip the write.
20
+ */
21
+ import { Document, YAMLMap, YAMLSeq, type DocumentOptions, type ParseOptions, type SchemaOptions } from 'yaml';
22
+ type YamlOptions = ParseOptions & DocumentOptions & SchemaOptions;
23
+ /** The result of {@link editYaml}. */
24
+ export interface YamlEditResult {
25
+ /** The new text — the original text when nothing changed. */
26
+ text: string;
27
+ /** False when the change left the document as it was. */
28
+ changed: boolean;
29
+ }
30
+ /** Parse options for every document faf edits. */
31
+ export declare const EDIT_OPTIONS: YamlOptions;
32
+ /** Parse `src` for editing; a file that is not valid YAML is refused (an
33
+ * Error whose `cause` is yaml's YAMLParseError, with its line). */
34
+ export declare function parseForEdit(src: string, name: string): Document;
35
+ /**
36
+ * Parse `text`, let `mutate` change the Document, and return the new text with
37
+ * every byte outside the changed nodes kept. `name` labels errors (a file that
38
+ * is not valid YAML is refused — nothing is guessed). When the change leaves
39
+ * the data as it was (the same values, whatever the nodes or their order),
40
+ * the original `text` is returned with `changed: false`.
41
+ */
42
+ export declare function editYaml(text: string, mutate: (doc: Document) => void, name?: string): YamlEditResult;
43
+ /** {@link editYaml}, also saying how the text was made: `spliced` is true when
44
+ * only the changed nodes were rewritten, false when faf fell back to the
45
+ * Document's own serialisation (or nothing changed). For tests. */
46
+ export declare function editYamlDetailed(text: string, mutate: (doc: Document) => void, name?: string): YamlEditResult & {
47
+ spliced: boolean;
48
+ };
49
+ /** Deep equality of plain parsed values (mappings compared key by key). */
50
+ export declare function sameJs(a: unknown, b: unknown): boolean;
51
+ /** How {@link applyMapData} treats keys the data does not name. */
52
+ export interface ApplyOptions {
53
+ /** Remove keys of a mapping that the data leaves out (or sets `undefined`).
54
+ * Default false: a key the data does not mention stays exactly as it is. */
55
+ prune?: boolean;
56
+ /** Leave every alias (`*name`) as written, whatever the data holds there:
57
+ * it is never expanded or replaced (see {@link keptAliases}). */
58
+ keepAliases?: boolean;
59
+ }
60
+ /**
61
+ * Set `node` to hold `value` with the fewest changes: a mapping is updated key
62
+ * by key, a list item by item (items still there are kept as they are), a
63
+ * scalar in place (its comment, quoting and position kept). Returns the node
64
+ * to store — `node` itself, or a new node when the shape changed.
65
+ */
66
+ export declare function applyValue(doc: Document, node: unknown, value: unknown, opts?: ApplyOptions): unknown;
67
+ /** Make `map` hold `data`: changed keys updated in place, new keys appended in
68
+ * `data`'s order. Keys `data` leaves out stay, unless `prune` is set. */
69
+ export declare function applyMapData(doc: Document, map: YAMLMap, data: Record<string, unknown>, opts?: ApplyOptions): void;
70
+ /**
71
+ * Merge `data` into `map`, whose parsed value (the file as it was read) is
72
+ * `before`, writing only the paths where `data` differs from `before`:
73
+ * - a key whose value in `data` equals its value in `before` is not touched
74
+ * — its node stays exactly as written, so an alias (`summary: *g`), a
75
+ * merge key or a comment on it survives even when `data` spells out the
76
+ * value the alias read as (a stale copy never replaces a live `*alias`);
77
+ * - a changed mapping is merged key by key the same way;
78
+ * - any other changed value is set with {@link applyValue};
79
+ * - an alias (`stack: *base`) is never replaced or expanded, at any depth:
80
+ * it stays as written, and a change `data` makes under it is not written
81
+ * ({@link keptAliases} lists those paths);
82
+ * - keys `data` leaves out (or sets `undefined`) stay; new keys are appended.
83
+ * This is how writeFaf applies full .faf data to an existing file.
84
+ */
85
+ export declare function mergeData(doc: Document, map: YAMLMap, before: unknown, data: Record<string, unknown>): void;
86
+ /** An alias faf left as written: where it is (`stack`, `key_files.2`) and
87
+ * what it says (`*base`). With `kind: 'anchor'` the path holds an anchor an
88
+ * alias reads (`alias` then says `&name`): `faf auto` would have filled it,
89
+ * which would change every alias that reads it, so it is left as written. */
90
+ export interface KeptAlias {
91
+ path: string;
92
+ alias: string;
93
+ kind?: 'alias' | 'anchor';
94
+ }
95
+ /** The aliases in `doc` where `data` changed the value the file held
96
+ * (`before`) and the alias does not read as the new value — a change under
97
+ * an alias that faf did not write, because it never replaces or expands an
98
+ * alias. A value `data` only repeats (a stale copy of what the alias read
99
+ * as) is not a change. */
100
+ export declare function keptAliases(doc: Document, data: unknown, before: unknown): KeptAlias[];
101
+ /**
102
+ * Put back every alias of `before` that `after` (the same Document, changed)
103
+ * holds something else in place of: faf never replaces an alias, so that
104
+ * path keeps the file's `*name` and its change is not written. A key the
105
+ * change removed stays removed. Returns the aliases put back.
106
+ */
107
+ export declare function restoreAliases(before: Document, after: Document): KeptAlias[];
108
+ /**
109
+ * Put back every node of `before` that carries an anchor an alias reads
110
+ * (`frontend: &x None` while `ui_library: *x`) and that `after` (the same
111
+ * Document, changed) holds something else in place of: a fill there would
112
+ * change the value of every alias that reads it, so faf leaves the node as
113
+ * written and its change is not written. Returns those paths (`kind:
114
+ * 'anchor'`, `alias` saying `&name`).
115
+ */
116
+ export declare function restoreAnchors(before: Document, after: Document): KeptAlias[];
117
+ /**
118
+ * Apply the change from `before` to `after` to `node`, touching nothing the
119
+ * change does not name: for mappings, only keys added, removed or changed
120
+ * between the two are written — a key in the file that neither side mentions
121
+ * stays as it is. Returns the node to store.
122
+ */
123
+ export declare function applyChange(doc: Document, node: unknown, before: unknown, after: unknown): unknown;
124
+ /** The mapping at `path` (created when absent or empty). Throws when something
125
+ * else is there — faf never replaces a scalar or a list to make room. */
126
+ export declare function mapAt(doc: Document, path: readonly string[], name: string): YAMLMap;
127
+ /**
128
+ * Apply the change `before` → `after` at `path` (see {@link applyChange}).
129
+ * The parent mapping is created when absent; `after === undefined` removes the
130
+ * key. Nothing is touched when the two are equal.
131
+ */
132
+ export declare function changeAt(doc: Document, path: readonly string[], before: unknown, after: unknown, name: string): void;
133
+ /** The list at `path`, created (as an empty list) when absent or empty.
134
+ * Throws when something else is there. */
135
+ export declare function seqAt(doc: Document, path: readonly string[], name: string): YAMLSeq;
136
+ export {};
@@ -15,11 +15,55 @@ export declare function assembleFreshFaf(dir: string): Record<string, unknown>;
15
15
  * consumers (faf-mcp's faf_auto) compose it instead of re-deriving it and
16
16
  * drifting (they used to merge assembleFreshFaf's slotignore'd output over the
17
17
  * existing file, losing interrogated facts such as a docker-compose Redis).
18
+ *
19
+ * Shape: `existing` must be a mapping (an empty document, null, reads as `{}`);
20
+ * a scalar or a list throws rather than being spread into character keys. A
21
+ * non-null scalar `project:` (older writers stored `project: <name>`) is lifted
22
+ * to `{ name: String(value) }`, so the name is kept and the rest can be filled.
23
+ *
24
+ * Words typed into a slot — a typed none (`None` / `N/A` / `not applicable`)
25
+ * or any other placeholder word (`unknown`, `"null"`), any case — are an
26
+ * empty slot: they score 0 until filled.
27
+ * - In a tech slot (every slot but the 6Ws) a repo fact fills it: "if it's
28
+ * a fact, fill the slot". With no fact the words stay exactly as the file
29
+ * has them, comment included; faf never writes `''` over them.
30
+ * - In a tech slot the file's app-type (`project.type`) leaves out, the
31
+ * app-type's decision is the fact: with no repo fact, the slot becomes
32
+ * `slotignored` (shown as N/A) in place of the words, an empty value or a
33
+ * placeholder. A real value there is kept. `slotignored` comes only from
34
+ * the app-type — in a slot the app-type uses, it is never written, even
35
+ * when detection reads the repo as another type; such a slot gets the
36
+ * repo's fact for it instead (`runtime: Go` from go.mod), or keeps what
37
+ * it had. A `project.type` faf does not know decides nothing, and neither
38
+ * does the `library` detection falls back to when the repo has no
39
+ * classifying signal.
40
+ * - In a 6W (`human_context.*`) the words are the person's: auto never
41
+ * replaces them (`faf go` asks).
42
+ * A slot that says `slotignored` under either of its names (`stack.db` for
43
+ * `stack.database`) gets no detected value under either. A slot the file
44
+ * names only by its Mk4 name (`stack.db`) keeps that name: faf adds no
45
+ * on-wire twin (`stack.database`), and a repo fact fills the name the file
46
+ * uses.
47
+ *
48
+ * The result is marked as a fill: writeFaf leaves a node with an anchor that
49
+ * an alias reads as written (`frontend: &x None` with `ui_library: *x`) —
50
+ * filling it would change every alias — and reports it like a kept alias.
18
51
  */
19
52
  export declare function updateExistingFaf(dir: string, existing: Record<string, unknown>): Record<string, unknown>;
20
- /** Fill empty/placeholder slots in `target` with values from `source`.
21
- * `target` wins when its slot is non-empty. Empty here is per `isPlaceholder`
22
- * (covers '', null, undefined, and known placeholder strings) — this is what
23
- * lets interrogated/detected values overwrite the empty-string defaults that
24
- * detectStack writes to human_context. */
53
+ /** Fill empty slots in `target` with values from `source`. `target` wins
54
+ * when its slot is non-empty. Empty here is '', null, undefined, an empty
55
+ * list or mapping — this is what lets interrogated/detected values overwrite
56
+ * the empty-string defaults that detectStack writes to human_context.
57
+ *
58
+ * Typed words — a typed none (`None`, `N/A`, `not applicable`) or any other
59
+ * placeholder word (`unknown`, `"null"`), any case — are an empty slot, but
60
+ * only a fact replaces them: in a tech slot (every slot but the 6Ws, under
61
+ * either of its names) a source value that is real content — not empty, not
62
+ * a placeholder, not `slotignored` — fills it. Anything else leaves the
63
+ * words exactly as they are: no fact, a 6W (`human_context.*`, the
64
+ * person's), or a place that is not a slot.
65
+ *
66
+ * A `_meta` the target already carries (the user's own) is never filled
67
+ * over: faf's runtime `_meta` from `source` is merged only into a target
68
+ * without one. */
25
69
  export declare function fillEmpties(target: Record<string, unknown>, source: Record<string, unknown>): Record<string, unknown>;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * The pure parts of `faf git` — no git, no network, no child process.
3
+ *
4
+ * `faf git <url>` validates the URL, clones the repo into a temp folder, and
5
+ * authors a fresh .faf from it with the same pipeline `faf auto` runs. The
6
+ * clone is the only step that runs git, and it stays in the CLI command. The
7
+ * rest lives here so consumers (claude-faf-mcp's faf_git, faf-mcp) compose it:
8
+ * they fetch the repo their own way, then call `authorFafFromRepo(dir)` and get
9
+ * the same .faf `faf git` would write — canonical keys, scored by faf's own
10
+ * scorer — instead of a hand-rolled extractor with its own schema.
11
+ */
12
+ import type { FafData } from '../core/types.js';
13
+ /**
14
+ * Validate + normalize a GitHub repo reference into a safe clone URL.
15
+ *
16
+ * A `faf git` URL is an untrusted CLI/MCP argument. This gate rejects any
17
+ * shell metacharacter or whitespace, and only ever returns a URL built from a
18
+ * strict allowlist pattern — defense in depth alongside the no-shell
19
+ * `execFileSync` clone (which already makes argument injection structurally
20
+ * impossible). Nothing carrying a metacharacter or control char ever reaches
21
+ * `git`: metachars are caught here; anything else fails the allowlist below.
22
+ *
23
+ * Accepts:
24
+ * owner/repo · github.com/owner/repo · https://github.com/owner/repo
25
+ * (optional `.git` suffix, optional trailing slash; full http(s) URLs to
26
+ * any host pass through with a single `.git` suffix)
27
+ *
28
+ * Throws on empty / malformed / unsafe input.
29
+ */
30
+ export declare function normalizeGitUrl(input: string): string;
31
+ /** The repo name from a normalized clone URL: .../owner/repo.git → repo. */
32
+ export declare function repoNameFromUrl(repoUrl: string): string;
33
+ export interface AuthorFafFromRepoOptions {
34
+ /** The repo's URL (a clone URL, or any form `normalizeGitUrl` accepts).
35
+ * When the fetched folder gives the project no name of its own (no
36
+ * package.json name), the project is named after the repo instead of the
37
+ * folder it was fetched into. */
38
+ repoUrl?: string;
39
+ }
40
+ /**
41
+ * Author a fresh .faf, as data, from a repo that is already on disk — the
42
+ * function form of `faf git`. Pass the folder you cloned or unpacked the repo
43
+ * into. Runs the full `faf auto` pipeline (detect → interrogate → slotignore →
44
+ * Turbo-Cat → Relentless) on that folder only, and returns the FafData —
45
+ * nothing is written, nothing is fetched, no git is run.
46
+ *
47
+ * Write it with `writeFaf` and score the written text with `scoreFafYaml`, so
48
+ * the score you report is the one faf reports.
49
+ */
50
+ export declare function authorFafFromRepo(dir: string, opts?: AuthorFafFromRepoOptions): FafData;
@@ -49,7 +49,8 @@ export declare function detectPolyglotLanguage(dir: string): string | null;
49
49
  export declare function readPackageJson(dir: string): PackageJson | null;
50
50
  /** Detect frameworks in a directory */
51
51
  export declare function detectFrameworks(dir: string): DetectedFramework[];
52
- /** Detect the primary language of a project */
52
+ /** Detect the primary language of a project from its manifests — `''` when
53
+ * there is no evidence. */
53
54
  export declare function detectLanguage(dir: string): string;
54
55
  /** Project-type detection result + rationale (the #found list).
55
56
  * Each `found` entry is a human-readable signal that contributed to the
@@ -72,13 +73,17 @@ export declare function detectProjectTypeWithRationale(dir: string): ProjectType
72
73
  * returns just the type string. New code should prefer
73
74
  * `detectProjectTypeWithRationale` to access the `#found` rationale. */
74
75
  export declare function detectProjectType(dir: string): string;
75
- /** Detect the runtime */
76
+ /** Detect the runtime from a runtime file or manifest — `''` when there is
77
+ * no evidence. */
76
78
  export declare function detectRuntime(dir: string): string;
77
- /** Detect package manager */
79
+ /** Detect the package manager from repo facts — a manifest that implies one,
80
+ * package.json's `packageManager` field, then a lockfile. `''` when there is
81
+ * no evidence: a package.json alone does not say which manager installs it. */
78
82
  export declare function detectPackageManager(dir: string): string;
79
83
  /** Detect CI/CD */
80
84
  export declare function detectCicd(dir: string): string | null;
81
- /** Detect hosting platform */
85
+ /** Detect hosting platform — from a platform's own config file. A bare
86
+ * Dockerfile is not one: it shows a container build, not where the app runs. */
82
87
  export declare function detectHosting(dir: string): string | null;
83
88
  /** Detect SvelteKit adapter from svelte.config.js */
84
89
  export declare function detectSvelteAdapter(dir: string): string | null;
@@ -1,3 +1,16 @@
1
1
  import type { FafData } from '../core/types.js';
2
- /** Auto-detect project stack and generate .faf data */
2
+ /** Detect the project stack from repo facts and build .faf data. A slot with
3
+ * no evidence is left empty (`''`) — never a default. */
3
4
  export declare function detectStack(dir: string): FafData;
5
+ /**
6
+ * {@link detectStack}'s data, and `facts`: the repo's fact for every stack
7
+ * slot (by field — `runtime`, `database`), whatever the app-type — `''` when
8
+ * the repo shows none. detectStack marks a slot its detected type leaves out
9
+ * `slotignored`; `faf auto` fills such a slot from `facts` when the file's own
10
+ * type uses it (a `backend` project whose repo reads as a CLI still gets
11
+ * `runtime: Go` from go.mod).
12
+ */
13
+ export declare function detectStackWithFacts(dir: string): {
14
+ data: FafData;
15
+ facts: Record<string, string>;
16
+ };
@@ -28,6 +28,16 @@
28
28
  * 😽 We are the FORMAT FREAKS - Quality over Quantity!
29
29
  * 🏆 199 formats - CHAMPIONSHIP GRADE FORMAT DETECTION
30
30
  */
31
+ /**
32
+ * A slot value here must name what the file itself proves, in a slot that
33
+ * value fills: `vercel.json` → hosting Vercel; `Dockerfile` → no hosting (it
34
+ * shows a container build, not where the app runs); `jest.config.js` →
35
+ * nothing for CI/CD (Jest is a test runner, not a pipeline). No generic words ('Cloud',
36
+ * 'Containerized', 'API Server'), no language in a framework or backend slot,
37
+ * and no `runtime: Node.js` from a framework config alone — package.json is
38
+ * that fact, and detectRuntime reads it. `faf auto` writes these values into
39
+ * project.faf, so a value that is not a fact about the repo is a guess.
40
+ */
31
41
  export interface FormatKnowledge {
32
42
  frameworks: string[];
33
43
  slots: Partial<ContextSlots>;
@@ -4,8 +4,12 @@
4
4
  * manifest interrogation the v6.0 rewrite narrowed to README+Cargo+package.json.
5
5
  *
6
6
  * v6-native sync port (the v5 engine was async; sync integrates cleanly with
7
- * `auto`). Two-layer: (1A) config files walking up to the monorepo .git
8
- * boundary, (1B) source extensions; priority-wins slot recommendations.
7
+ * `auto`). Two-layer: (1A) config files in the project directory itself,
8
+ * (1B) source extensions below it; priority-wins slot recommendations.
9
+ *
10
+ * Boundary: detection never looks above the project directory. A parent's
11
+ * tsconfig.json, Cargo.toml or vercel.json is another project's evidence —
12
+ * a monorepo package, a subfolder or a new folder under ~ must not inherit it.
9
13
  *
10
14
  * Used as the LOWEST-precedence filler in `auto`: v6's specific detection wins;
11
15
  * Turbo-Cat fills only the slots still empty (esp. non-npm stacks).
@@ -1,4 +1,5 @@
1
1
  /** .fafm knowledge-profile library (TS) — INTEROP with claude-fafm-sdk 1.0 */
2
2
  export { PRIORITY_ORDER, PRIORITY_RANK, KNOWLEDGE_TYPES, type Fact, type Priority, type Profile, type SoulDoc, } from './types.js';
3
3
  export { Soul, utcNow, canonicalPriority, factFromObj, factToObj, } from './soul.js';
4
+ export type { RecallOptions } from './soul.js';
4
5
  export { fromClaudeDir, DEFAULT_SKIP } from './from-claude-dir.js';
@@ -1,12 +1,27 @@
1
1
  /**
2
2
  * Soul — local .fafm model (TS mirror of claude-fafm-sdk 1.0 Soul).
3
3
  * INTEROP: load/save fidelity, residual preserve, recall SoT.
4
+ *
5
+ * A soul loaded from a file keeps that file's text. `save` writes only what
6
+ * changed since the load (or the last save) into it — through the YAML
7
+ * Document, so comments, key order, quoting, the `version`, a missing
8
+ * `profile`, a hand-kept `index` and every unknown key stay as they were.
9
+ * Known keys faf does not model in the shape the file has them (a `facts` or
10
+ * `index` written as a mapping, `sessions` as a mapping, …) are kept verbatim;
11
+ * a change that would have to rewrite one is refused, with nothing written.
4
12
  */
5
13
  import { type Fact, type SoulDoc } from './types.js';
6
14
  export declare function utcNow(): string;
7
15
  export declare function canonicalPriority(p: string | null | undefined): string;
8
16
  export declare function factFromObj(obj: unknown): Fact;
9
17
  export declare function factToObj(f: Fact): unknown;
18
+ /** Filters for {@link Soul.recall}. */
19
+ export interface RecallOptions {
20
+ tags?: string[];
21
+ type?: string;
22
+ minPriority?: string;
23
+ limit?: number | null;
24
+ }
10
25
  export declare class Soul {
11
26
  namepoint: string;
12
27
  profile: string;
@@ -21,6 +36,20 @@ export declare class Soul {
21
36
  private _custom;
22
37
  private _extra;
23
38
  private _memoryExtra;
39
+ private _version;
40
+ /** The file the soul was loaded from or last saved to. Undefined while the
41
+ * soul was made with no file (in memory, not saved yet): its first save
42
+ * then refuses a file already at the path — one that appeared since the
43
+ * caller found none — unless `replace`. */
44
+ private _origin;
45
+ /** Each loaded fact's item position in the file's memory.facts. */
46
+ private _factAt;
47
+ /** The index was faf-derived when the soul was loaded (or made, or last
48
+ * saved) — the default save keeps it in step with the facts. */
49
+ private _indexDerived;
50
+ /** The index as it was then: an index changed in memory since is the
51
+ * caller's, and the default save leaves it as it is. */
52
+ private _indexAtRecord;
24
53
  constructor(namepoint: string, opts?: {
25
54
  profile?: string;
26
55
  facts?: Fact[];
@@ -40,18 +69,89 @@ export declare class Soul {
40
69
  get custom(): Record<string, unknown>;
41
70
  get extra(): Record<string, unknown>;
42
71
  get memoryExtra(): Record<string, unknown>;
72
+ /**
73
+ * Load a soul. The root must be a mapping (anything else is refused). Known
74
+ * keys in a shape faf does not model — `memory.facts` or `index` written as
75
+ * a mapping, `memory.sessions` as a mapping, `memory.preferences` as a list,
76
+ * `memory` itself as a list — load as empty and stay in the file verbatim;
77
+ * so do list items in `memory.facts` that are not facts.
78
+ */
43
79
  static load(path: string): Soul;
80
+ private static fromText;
81
+ /** Note whether the index is faf-derived now (at load, and after a save). */
82
+ private recordIndex;
83
+ /** Take a fact read from item `i` of the file's memory.facts. */
84
+ private adopt;
44
85
  static fromFile(path: string): Soul;
45
86
  toDoc(): SoulDoc;
46
87
  toYaml(): string;
88
+ /** The index faf derives from the facts (INTEROP §5 formula,
89
+ * `${id ?? '?'} — ${text[:width]}` per fact) — without changing the soul. */
90
+ derivedIndex(width?: number): string[];
91
+ /** True when the stored index is exactly the one faf derives from the facts
92
+ * (so faf wrote it, and may rewrite it). A hand-kept index is not. */
93
+ indexIsDerived(width?: number): boolean;
47
94
  rebuildIndex(width?: number): string[];
95
+ /**
96
+ * Write the soul — atomically (a failure leaves the original as it was)
97
+ * and never through a link that leaves the folder or dangles.
98
+ *
99
+ * A soul loaded from a file is written as that file's text with only what
100
+ * changed since the load (or the last save) edited into it; a save that
101
+ * changes nothing writes nothing.
102
+ *
103
+ * The index: `reindex: true` rebuilds it from the facts, `reindex: false`
104
+ * keeps it as it is. Left out, the index is rebuilt only when it was
105
+ * faf-derived — {@link indexIsDerived} when the soul was loaded or last
106
+ * saved; a new soul counts as derived unless it was given an index of its
107
+ * own — and has not been changed in memory since. A hand-kept index is
108
+ * never touched.
109
+ * Refused, with nothing written: a change to a known key the file holds in
110
+ * a shape faf does not model (adding a fact to `memory.facts` written as a
111
+ * mapping, say); a save over the file the soul was loaded from (or last
112
+ * saved to) when that file changed on disk since — SafePathError `changed`,
113
+ * so an edit made meanwhile is never written over; and a save of a soul
114
+ * made with no file (or to a path it was not loaded from) when a file is
115
+ * already there — the same `changed` refusal, so a soul.fafm that appeared
116
+ * after the caller found none is kept. `replace: true` is the explicit
117
+ * overwrite of such a file (`faf memory convert --force`).
118
+ */
48
119
  save(path: string, opts?: {
49
120
  reindex?: boolean;
121
+ replace?: boolean;
50
122
  }): string;
51
123
  toFile(path: string, opts?: {
52
124
  reindex?: boolean;
125
+ replace?: boolean;
53
126
  }): string;
127
+ /** After a save: the written text (at `real`) is the new origin. */
128
+ private settle;
129
+ /** The soul's modelled state as plain values. */
130
+ private state;
131
+ /** Refuse a change that would rewrite a key kept verbatim. */
132
+ private static refuse;
133
+ /** Edit the loaded Document with what changed since `origin.base`; returns
134
+ * each fact's item position in memory.facts afterwards. */
135
+ private applyTo;
136
+ private applyMemory;
137
+ /** Which facts are still at their place in the file, and which are new.
138
+ * Kept facts must be in the file's order, new ones after them. */
139
+ private sortFacts;
140
+ /** Facts: changed facts are edited where they are (only the fields that
141
+ * changed), deleted ones removed, new ones appended. List items that are
142
+ * not facts are never touched. */
143
+ private applyFacts;
144
+ /** Insert or overwrite a Fact by id, preserving every field it carries (its
145
+ * timestamp included) — the INTEROP merge primitive: on an existing id the
146
+ * whole Fact is replaced. `etch` merges instead. */
54
147
  add(fact: Fact): Fact;
148
+ /**
149
+ * Write a fact. With an `id` that is already in the soul, the fact is
150
+ * updated in place: the text and timestamp are new, and of the other fields
151
+ * only those passed here change — links, source, tags, type, priority and
152
+ * any extra fields the fact carries are kept (a priority is never lowered
153
+ * unless one is passed). Otherwise the fact is appended.
154
+ */
55
155
  etch(text: string, opts?: {
56
156
  id?: string;
57
157
  type?: string;
@@ -60,12 +160,14 @@ export declare class Soul {
60
160
  links?: string[];
61
161
  source?: string;
62
162
  }): Fact;
63
- recall(query?: string | null, opts?: {
64
- tags?: string[];
65
- type?: string;
66
- minPriority?: string;
67
- limit?: number | null;
68
- }): Fact[];
163
+ /**
164
+ * Deterministic recall (INTEROP §6). Call it as `recall(query, filters)` or
165
+ * `recall({ query, ...filters })`. Bare-string facts are facts: their text
166
+ * is matched and returned like any other.
167
+ */
168
+ recall(query?: string | null | (RecallOptions & {
169
+ query?: string | null;
170
+ }), opts?: RecallOptions): Fact[];
69
171
  getFact(id: string): Fact | null;
70
172
  deleteFact(id: string): boolean;
71
173
  }
package/dist/index.d.ts CHANGED
@@ -1,19 +1,25 @@
1
1
  export type { SlotState, SlotCategory, SlotDef, KernelScoreResult, ScoreResult, TierInfo, FafData, FafbInfo, DetectedFramework, FrameworkSignature, Signal, SignalType, } from './core/types.js';
2
2
  export { SLOTS, BASE_SLOTS, ENTERPRISE_SLOTS, SLOT_BY_PATH, slotsByCategory, PLACEHOLDERS, isPlaceholder } from './core/slots.js';
3
+ export { EXPLICIT_NONE, SLOTIGNORED, isExplicitNone } from './core/slots.js';
3
4
  export { TIERS, getTier, getNextTier } from './core/tiers.js';
4
5
  export { FAF_HEX } from './ui/colors.js';
5
6
  export { enrichScore, scoreFafYaml } from './core/scorer.js';
7
+ export { scoreText } from './core/scorer.js';
6
8
  export { validateFaf } from './core/schema.js';
7
9
  export { findFafFile, readFaf, readFafRaw } from './interop/faf.js';
10
+ export { isNonProjectRoot } from './core/cwd-guard.js';
8
11
  export { computeDrift } from './core/drift.js';
9
12
  export type { DriftReport, DriftTarget, DriftStatus } from './core/drift.js';
10
13
  export { renderProjectHtml, generateProjectHtml, writeProjectHtml } from './interop/projecthtml.js';
11
- export { buildServerCard, generateServerCard, writeServerCard, fafContextBlock, registryMeta, registryName, REGISTRY_PUBLISHER_KEY, } from './interop/servercard.js';
12
- export type { ServerCardOptions } from './interop/servercard.js';
14
+ export type { ProjectHtmlWriteOptions } from './interop/projecthtml.js';
15
+ export { buildServerCard, generateServerCard, writeServerCard, fafContextBlock, registryMeta, registryName, registryTitle, REGISTRY_PUBLISHER_KEY, } from './interop/servercard.js';
16
+ export type { ServerCardOptions, CardWriteOptions } from './interop/servercard.js';
17
+ export { patchServerJson } from './interop/servercard.js';
18
+ export type { ServerJsonIdentity } from './interop/servercard.js';
13
19
  export { projectCards, readFafa, findFafaFile, buildA2ACard, generateA2ACard, upsertCatalog, A2A_CONTEXT_URI, } from './interop/cards.js';
14
20
  export type { FafaDoc, CardTarget, ProjectedCards } from './interop/cards.js';
15
21
  export { Soul as FafmSoul, fromClaudeDir, canonicalPriority, utcNow, PRIORITY_ORDER, PRIORITY_RANK, KNOWLEDGE_TYPES, } from './fafm/index.js';
16
- export type { Fact as FafmFact, SoulDoc, Profile as FafmProfile } from './fafm/index.js';
22
+ export type { Fact as FafmFact, SoulDoc, Profile as FafmProfile, RecallOptions as FafmRecallOptions } from './fafm/index.js';
17
23
  export type { InterviewQuestion, InterviewOption, HumanSlotPath, SourcedSlotPath, GoalSeed, TableOf8, TableOf8Row, BoxStatus } from './core/interview.js';
18
24
  export { INTERVIEW, SIX_WS_INTERVIEW, STACK_INTERVIEW, INTERVIEW_BY_PATH, INTERVIEW_PATHS, INTERVIEW_VERSION, questionForSlot, interviewForMissing, seedSixWsFromGoal, buildTableOf8, } from './core/interview.js';
19
25
  export type { LoopStatus, LoopGaps, LoopVerdict, LoopRunStatus, LoopDeps, LoopRunOptions, LoopRunResult } from './core/loop.js';
@@ -25,6 +31,10 @@ export type { TurboCatResult, DiscoveredFormat } from './detect/turbo-cat.js';
25
31
  export { relentlessContext, relentlessContextDetailed } from './detect/relentless.js';
26
32
  export type { SeededContext, SeededContextDetailed, SourcedValue } from './detect/relentless.js';
27
33
  export { assembleFreshFaf, updateExistingFaf, fillEmpties } from './detect/assemble.js';
34
+ export { normalizeGitUrl, repoNameFromUrl, authorFafFromRepo } from './detect/git-repo.js';
35
+ export type { AuthorFafFromRepoOptions } from './detect/git-repo.js';
36
+ export { FafDNAManager } from './core/faf-dna.js';
37
+ export type { FafDNA, BirthCertificate, VersionEntry, Milestone } from './core/faf-dna.js';
28
38
  export { renderAgentsMd, writeAgentsMd } from './interop/agents.js';
29
39
  export { renderGeminiMd, writeGeminiMd } from './interop/gemini.js';
30
40
  export { renderCursorrules, writeCursorrules } from './interop/cursorrules.js';
@@ -32,6 +42,17 @@ export { renderCopilotInstructions, writeCopilotInstructions } from './interop/c
32
42
  export { renderClaudeMd, writeClaudeMd, readClaudeMd, parseClaudeMd, fafMetaTag } from './interop/claude.js';
33
43
  export type { FafMetaOpts } from './interop/claude.js';
34
44
  export { injectFafBlock, findFafBlock, FAF_START, FAF_END } from './interop/inject.js';
45
+ export type { InjectOptions } from './interop/inject.js';
46
+ export { legacyStampNote, legacyStampNoteAt } from './interop/inject.js';
47
+ export { claudeProjectId, claudeProjectRoot, resolveClaudeMemoryDir, resolveClaudeMemoryPath, renderClaudeMemory, writeClaudeMemory, claudeMemoryStatus, } from './interop/claude-memory.js';
48
+ export type { ClaudeMemoryOptions, ClaudeMemoryResult, ClaudeMemoryAction, ClaudeMemoryStatus } from './interop/claude-memory.js';
49
+ export { resolveInside, safeWriteFile, SafePathError } from './core/safe-write.js';
50
+ export { readUtf8 } from './core/safe-write.js';
51
+ export type { ResolveInsideOptions, SafeWriteOptions, SafePathReason } from './core/safe-write.js';
52
+ export { safeReplaceOwned, makeDirInside, safeUnlink } from './core/safe-write.js';
53
+ export type { ReplaceOwnedOptions } from './core/safe-write.js';
35
54
  export { enrichFromRepo } from './detect/enrich.js';
36
55
  export { serializeFaf, writeFaf } from './interop/faf.js';
56
+ export { updateFafFile } from './interop/faf.js';
57
+ export type { UpdateFafResult, WriteFafOptions, KeptAlias } from './interop/faf.js';
37
58
  export * as kernel from './wasm/kernel.js';