@skanl/brambo-projection 0.1.1

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.
@@ -0,0 +1,17 @@
1
+ export { runProjection, groupByKind, hasFileChangedSince, resolveProjectionMode } from './engine.ts';
2
+ export type { NativeFileSnapshot, ProjectionMode, ProjectionRun, RunProjectionOptions, } from './engine.ts';
3
+ export { WRITABLE_CONFIG_KEYS, configPathFor, setConfigValue, type ConfigWriteOptions, type ConfigWriteResult, type WritableConfigKey, } from './config-write.ts';
4
+ export { runRemediation } from './remediate.ts';
5
+ export type { AdoptRemediationOptions, DiscardRemediationOptions, LegacyBlockLocation, ReleaseRemediationOptions, RepairRemediationOptions, RunRemediationOptions, } from './remediate.ts';
6
+ export { ProjectionLedger, hashOwnedBytes, hashOwnedText, resolveOwnedPath, sameOwnedPath } from './ledger.ts';
7
+ export type { ProjectionLedgerOptions, ProjectionLedgerRead, ProjectionLedgerScope, ProjectionLedgerState, } from './ledger.ts';
8
+ export { createProjectionTargetFromTraits, readNativeMcpEntries, scanLegacyBramboBlock } from './formats.ts';
9
+ export type { FileFormat, LegacyBramboBlock, LegacyBramboScan, NativeEntryShape, NativeMcpRead, ProjectionTargetTraits, TraitTargetOptions, } from './formats.ts';
10
+ export { createClaudeMcpTarget, CLAUDE_MCP_TARGET_ID, CLAUDE_MCP_TRAITS } from './targets/claude-mcp.ts';
11
+ export type { ClaudeMcpTargetOptions } from './targets/claude-mcp.ts';
12
+ export { createCodexConfigTarget, CODEX_CONFIG_TARGET_ID, CODEX_CONFIG_TRAITS } from './targets/codex-config.ts';
13
+ export type { CodexConfigTargetOptions } from './targets/codex-config.ts';
14
+ export { createOpenCodeConfigTarget, OPENCODE_CONFIG_TARGET_ID, OPENCODE_CONFIG_TRAITS, } from './targets/opencode-config.ts';
15
+ export type { OpenCodeConfigTargetOptions } from './targets/opencode-config.ts';
16
+ export { CLAUDE_SKILLS_TARGET_ID, CLAUDE_SKILLS_TRAITS, CODEX_SKILLS_TARGET_ID, CODEX_SKILLS_TRAITS, OPENCODE_SKILLS_TARGET_ID, OPENCODE_SKILLS_TRAITS, SKILL_ENTRY_FILE, createClaudeSkillsTarget, createCodexSkillsTarget, createOpenCodeSkillsTarget, createSkillsTargetFromTraits, } from './targets/skills.ts';
17
+ export type { SkillsTargetOptions, SkillsTargetTraits } from './targets/skills.ts';
package/dist/index.js ADDED
@@ -0,0 +1,15 @@
1
+ export { runProjection, groupByKind, hasFileChangedSince, resolveProjectionMode } from './engine.js';
2
+ // `atomicWriteText` is deliberately NOT here. A previous story un-exported it so
3
+ // `@skanl/brambo-environment` could not reach it, and `packages/environment/test/guard.test.ts`
4
+ // is the clause that notices if it comes back — the ledger is the sole authority
5
+ // for what brambo writes into a vendor's file. Brambo's OWN configuration document
6
+ // is a different thing and needs the same symlink-resolving write, so the WRITER
7
+ // lives here, beside the primitive, and only the writer is published.
8
+ export { WRITABLE_CONFIG_KEYS, configPathFor, setConfigValue, } from './config-write.js';
9
+ export { runRemediation } from './remediate.js';
10
+ export { ProjectionLedger, hashOwnedBytes, hashOwnedText, resolveOwnedPath, sameOwnedPath } from './ledger.js';
11
+ export { createProjectionTargetFromTraits, readNativeMcpEntries, scanLegacyBramboBlock } from './formats.js';
12
+ export { createClaudeMcpTarget, CLAUDE_MCP_TARGET_ID, CLAUDE_MCP_TRAITS } from './targets/claude-mcp.js';
13
+ export { createCodexConfigTarget, CODEX_CONFIG_TARGET_ID, CODEX_CONFIG_TRAITS } from './targets/codex-config.js';
14
+ export { createOpenCodeConfigTarget, OPENCODE_CONFIG_TARGET_ID, OPENCODE_CONFIG_TRAITS, } from './targets/opencode-config.js';
15
+ export { CLAUDE_SKILLS_TARGET_ID, CLAUDE_SKILLS_TRAITS, CODEX_SKILLS_TARGET_ID, CODEX_SKILLS_TRAITS, OPENCODE_SKILLS_TARGET_ID, OPENCODE_SKILLS_TRAITS, SKILL_ENTRY_FILE, createClaudeSkillsTarget, createCodexSkillsTarget, createOpenCodeSkillsTarget, createSkillsTargetFromTraits, } from './targets/skills.js';
@@ -0,0 +1,193 @@
1
+ import type { ProjectionLedgerRecord, ProjectionWarning } from '@skanl/brambo-contracts';
2
+ import type { StaleLockBreak } from '@skanl/brambo-lock';
3
+ /**
4
+ * Hash of the CANONICAL form of the text brambo placed at a native location.
5
+ *
6
+ * EOL is normalised because a file that git, an editor or a formatter rewrites
7
+ * from LF to CRLF has not been edited in any sense a vendor can observe — and
8
+ * `~/.claude.json` is rewritten by Claude Code itself. Treating that as an edit
9
+ * would make brambo disown every entry it has in the file. Format-specific
10
+ * canonicalisation (indentation, key order) happens in the strategies, which
11
+ * are the only code that knows what "the same entry" means per format.
12
+ */
13
+ export declare function hashOwnedText(text: string): string;
14
+ /**
15
+ * Hash of the exact BYTES brambo copied to a path.
16
+ *
17
+ * Deliberately not {@link hashOwnedText}: a materialised file is copied verbatim
18
+ * from a source brambo does not author, so "the same file" means the same bytes.
19
+ * Normalising EOL here would let brambo overwrite a file whose line endings a
20
+ * user deliberately changed, and — far worse, since this is the delete path —
21
+ * let it REMOVE one.
22
+ */
23
+ export declare function hashOwnedBytes(bytes: Uint8Array): string;
24
+ /**
25
+ * The EOL-normalised form of the same bytes, for the OVERWRITE comparison only.
26
+ *
27
+ * Never for a removal. A removal is decided byte for byte, because a false
28
+ * match there precedes `rm`; an overwrite decided byte for byte instead makes
29
+ * a skills root kept under `core.autocrlf` report every brambo file as edited
30
+ * forever, and the product has no adopt, force or reclaim path out of that.
31
+ */
32
+ export declare function canonicalBytesHash(bytes: Uint8Array): string;
33
+ /** Ownership keys must be one canonical spelling of a path, never two. */
34
+ export declare function resolveOwnedPath(filePath: string): string;
35
+ /** win32 paths differ in drive-letter and directory casing between processes. */
36
+ export declare function sameOwnedPath(left: string, right: string): boolean;
37
+ /**
38
+ * Whether `path` is strictly inside `root`. Both arguments must already be
39
+ * resolved — this predicate answers about paths, not about strings a caller
40
+ * hopes are paths.
41
+ *
42
+ * It lives beside {@link resolveOwnedPath} because it is the second half of the
43
+ * same rule: a path brambo acts on is CANONICALISED and then proven to be inside
44
+ * the location brambo owns. Every caller that skips either half has been a
45
+ * user-data defect — the removal path took raw ledger strings straight to `rm`,
46
+ * and a relative one resolved against the process working directory.
47
+ */
48
+ export declare function isUnderRoot(path: string, root: string): boolean;
49
+ /**
50
+ * The right to replace the WHOLE ownership document without merging.
51
+ *
52
+ * A module-scope symbol rather than a convention: the only way to hold it is to
53
+ * import it, so "one caller" stops being a claim a text scan makes and becomes
54
+ * something the runtime enforces. `test/guard.test.ts` pins who imports it.
55
+ */
56
+ export declare const LEDGER_REPAIR_AUTHORITY: unique symbol;
57
+ export type ProjectionLedgerState = 'absent' | 'readable' | 'unreadable';
58
+ export interface ProjectionLedgerRead {
59
+ /** `unreadable` means the file exists but brambo must not write over it. */
60
+ readonly state: ProjectionLedgerState;
61
+ readonly records: readonly ProjectionLedgerRecord[];
62
+ /**
63
+ * Records `records` rejected, reduced to the smallest claim brambo can still
64
+ * ACT on. Empty unless the document holds damage, and read by `repair` alone:
65
+ * every other caller wants only what brambo can vouch for.
66
+ */
67
+ readonly salvaged: readonly ProjectionLedgerRecord[];
68
+ readonly warnings: readonly ProjectionWarning[];
69
+ }
70
+ /** Everything one target claims in one file — the unit a run replaces. */
71
+ export interface ProjectionLedgerScope {
72
+ readonly targetId: string;
73
+ readonly filePath: string;
74
+ }
75
+ export interface ProjectionLedgerOptions {
76
+ /** Defaults to the OS home directory. */
77
+ readonly homeDir?: string;
78
+ /** Overrides the whole path; the default is `<home>/.brambo/projection-ledger.json`. */
79
+ readonly filePath?: string;
80
+ /** Bounded wait for the cross-process lock before a coded CONTENTION refusal. */
81
+ readonly lockTimeoutMs?: number;
82
+ /**
83
+ * Observes every stale/corrupt-lock break performed on the way to a write.
84
+ *
85
+ * A break is brambo deciding that a lock left behind by a dead process no
86
+ * longer protects anything. That decision is REPORTED rather than silent,
87
+ * because it is the one moment where the outer boundary steps aside.
88
+ */
89
+ readonly onStaleLockBreak?: (broken: StaleLockBreak) => void;
90
+ }
91
+ /**
92
+ * The hash brambo writes for a claim it can still ADDRESS but can no longer
93
+ * VOUCH for.
94
+ *
95
+ * Deliberately not hex. A real `contentHash` is `sha256` output, so this can
96
+ * never compare equal to one by accident — the never-matching property is
97
+ * structural rather than improbable. `isLedgerRecord` asks only for a non-empty
98
+ * string, and nothing anywhere validates the shape, so this survives a round
99
+ * trip and reads as `edited` forever: brambo knows which bytes the claim covers
100
+ * and admits it does not know what it wrote there.
101
+ */
102
+ export declare const UNVOUCHED_CONTENT_HASH = "unreadable-after-repair";
103
+ /**
104
+ * The exact bytes of the ledger document for a record set.
105
+ *
106
+ * Exported so a remediation can PREDICT the document it is about to write and
107
+ * report the byte delta before writing it, using the same serialisation the
108
+ * write itself performs. A second spelling here would let a preview report a
109
+ * size the act does not produce.
110
+ */
111
+ export declare function serialiseLedgerDocument(records: readonly ProjectionLedgerRecord[]): string;
112
+ export declare class ProjectionLedger {
113
+ #private;
114
+ readonly filePath: string;
115
+ constructor(options?: ProjectionLedgerOptions);
116
+ /** Never throws: the three states are what callers must distinguish. */
117
+ read(): Promise<ProjectionLedgerRead>;
118
+ /**
119
+ * Replaces the records this caller TOOK A POSITION ON inside one scope,
120
+ * keeping every other claim — including claims inside the same scope that
121
+ * landed after the caller's snapshot was taken.
122
+ *
123
+ * `examined` is the third parameter and it is required, because omission used
124
+ * to mean deletion and nobody could see it. The caller passes the entry ids it
125
+ * read out of its own ledger snapshot for this scope; `update` adds the ids it
126
+ * is being handed now. An id in neither set belongs to a writer this caller
127
+ * never saw, and survives untouched.
128
+ *
129
+ * WITHOUT IT, A CONCURRENT RUN ERASES CLAIMS IT DELIBERATELY LEFT ALONE.
130
+ * Measured on the binary: ten rounds of two concurrent `brambo init` over one
131
+ * home lost 20 of 40 claims, 20 of 20 processes exited 0, and no stderr line
132
+ * named foreign, skip, collision or ledger; the one-process control lost 0 of
133
+ * 40. A wider run lost 72 of 144 and left 16 vendor entries owned by nobody,
134
+ * after which `brambo doctor` exited 0 reporting `"drift": []` and a later
135
+ * `remove` + `init` could no longer take those entries back out — brambo had
136
+ * permanently lost the ability to undo bytes it wrote.
137
+ *
138
+ * The mechanism is a granularity mismatch, not a lock. Process B reads the
139
+ * ledger before A persists, so B's snapshot holds none of A's claims. B then
140
+ * reads the vendor file, which by now holds A's entries, and classifies them
141
+ * as foreign — CORRECTLY, since nothing B can see claims them — so they never
142
+ * reach `projected.records`. The merge decided per entry; the write then
143
+ * replaced the whole scope. B left A's bytes alone and erased A's claim in the
144
+ * same breath. Keying the drop to what the caller examined is what makes the
145
+ * two agree.
146
+ *
147
+ * Deliberately NOT one write per entry: that shape preserves the same claims,
148
+ * but measured 99 writes per init instead of 6 (p50 61.5 ms each, 69x the
149
+ * ledger phase) and opened a contention cliff — 17 of 48 scopes failing at
150
+ * eight concurrent inits against 0 of 48 today. This keeps one write per scope.
151
+ *
152
+ * Still serialised against concurrent calls on this instance, and still NOT a
153
+ * fix for the orphan window above it: `engine.ts` writes the vendor file
154
+ * before it reaches this method, so a persist that throws — or a crash in
155
+ * between — leaves bytes no record claims. That is a different defect with a
156
+ * different fix, and it is open.
157
+ */
158
+ update(scope: ProjectionLedgerScope, records: readonly ProjectionLedgerRecord[], examined: readonly string[]): Promise<void>;
159
+ /**
160
+ * Replaces ONE entry's record inside one scope, reading the current document
161
+ * INSIDE the queue.
162
+ *
163
+ * The granularity is the point. A caller that read the ledger, decided, and
164
+ * then handed `update` a whole replacement set for the scope would resurrect
165
+ * every claim another writer legitimately dropped in between — brambo would
166
+ * then claim a path it does not own, which on the materialisation path is a
167
+ * delete authority. Only the named entry moves here; every sibling claim is
168
+ * whatever the document says at the moment of the write.
169
+ *
170
+ * `record === undefined` drops the entry instead of replacing it.
171
+ */
172
+ updateEntry(scope: ProjectionLedgerScope, entryId: string, record: ProjectionLedgerRecord | undefined): Promise<void>;
173
+ /**
174
+ * Replaces the WHOLE document with whatever `select` returns for the document
175
+ * as it is INSIDE the queue.
176
+ *
177
+ * This is the one write that does not merge, and it exists for exactly one
178
+ * caller: the user-named `repair` remediation, which is how a ledger holding
179
+ * records brambo cannot read stops being a state with no exit. Nothing else may
180
+ * use it — `update` is the merging write every projection performs, and its
181
+ * refusal to overwrite an unreadable ledger is a load-bearing guarantee that
182
+ * this method deliberately does not have. `test/guard.test.ts` pins the caller
183
+ * list, because a second one would silently reintroduce the orphan-every-claim
184
+ * failure Story 2.8 declared terminal.
185
+ *
186
+ * `select` runs INSIDE the queue and is handed the read the write will be
187
+ * based on. A caller that read the document itself and passed the result would
188
+ * destroy every claim written in between — with no merge to save it, which is
189
+ * exactly what makes this method the dangerous one. It may throw to abort the
190
+ * write, which is how `repair` refuses when the document moved under it.
191
+ */
192
+ rewriteAll(authority: typeof LEDGER_REPAIR_AUTHORITY, select: (read: ProjectionLedgerRead) => readonly ProjectionLedgerRecord[]): Promise<void>;
193
+ }