Skip to content

Commit 626943b

Browse files
[Agent: Claude] feat(lws): .lwsowner sidecar primitives + ensureDeclaredType merge helper
- ownerStorePath/readOwners/writeOwners (URI-validated, dedup, ≥1) - ensureDeclaredType: read-merge-write marker stamp for the boot backfill Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 4d01f41 commit 626943b

2 files changed

Lines changed: 74 additions & 0 deletions

File tree

src/lws/type-metadata.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,42 @@ export async function readDeclaredTypes(storage, storagePath) {
5050
catch { return []; }
5151
}
5252

53+
// Merge one type into `.lwstypes` without clobbering what's there —
54+
// captureDeclaredTypes overwrites by design (provisioning), but the boot
55+
// backfill (governance round 2026-07-22) touches roots that may already
56+
// carry client-declared types. Returns true only when it wrote.
57+
export async function ensureDeclaredType(storage, storagePath, typeUri) {
58+
const existing = await readDeclaredTypes(storage, storagePath);
59+
if (existing.includes(typeUri)) return false;
60+
await storage.write(typeStorePath(storagePath), Buffer.from(JSON.stringify([...existing, typeUri])));
61+
return true;
62+
}
63+
64+
// Per-storage owner record (governance round 2026-07-22): solid:owner URIs
65+
// for a storage root, System-Managed like `.lwstypes`. In-storage (not
66+
// config) because ownership travels with the data on re-homing; the
67+
// deployment operator (schema:provider) is config precisely because it
68+
// does not. Design: docs/superpowers/specs/2026-07-22-* in lws-pod.
69+
export function ownerStorePath(storagePath) {
70+
return storagePath + '.lwsowner';
71+
}
72+
73+
export async function readOwners(storage, storagePath) {
74+
const p = ownerStorePath(storagePath);
75+
if (!(await storage.exists(p))) return [];
76+
const buf = await storage.read(p);
77+
if (!buf) return [];
78+
try { const arr = JSON.parse(buf.toString('utf8')); return Array.isArray(arr) ? arr.filter((o) => isAbsoluteUri(o)) : []; }
79+
catch { return []; }
80+
}
81+
82+
export async function writeOwners(storage, storagePath, ownerUris) {
83+
const clean = [];
84+
for (const o of (ownerUris || [])) if (isAbsoluteUri(o) && !clean.includes(o)) clean.push(o);
85+
if (!clean.length) return; // ≥1 owner or no record
86+
await storage.write(ownerStorePath(storagePath), Buffer.from(JSON.stringify(clean)));
87+
}
88+
5389
// Earned conformsTo provenance (System-Managed): which profile a member's
5490
// CONTAINER declared at the moment the member was admitted. Distinct from
5591
// the client-managed `.meta` dct:conformsTo (declared binding intent) — a

test/lwsowner-metadata.test.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// test/lwsowner-metadata.test.js
2+
// Governance round (2026-07-22): .lwsowner sidecar primitives + the
3+
// read-merge-write marker helper the boot backfill depends on.
4+
import { describe, it, before } from 'node:test';
5+
import assert from 'node:assert/strict';
6+
import { startLwsPod } from './helpers.js';
7+
import * as storage from '../src/storage/filesystem.js';
8+
import {
9+
ownerStorePath, readOwners, writeOwners, ensureDeclaredType,
10+
readDeclaredTypes, typeStorePath, LWS_STORAGE,
11+
} from '../src/lws/type-metadata.js';
12+
13+
describe('.lwsowner primitives + marker merge', () => {
14+
let pod, root;
15+
before(async (t) => { pod = await startLwsPod(t, 'govmeta'); root = `/${pod.podName}/`; });
16+
17+
it('writeOwners/readOwners round-trip, dedupe, URI-only', async () => {
18+
await writeOwners(storage, root, [pod.webId, pod.webId, 'not a uri']);
19+
assert.deepEqual(await readOwners(storage, root), [pod.webId]);
20+
assert.equal(ownerStorePath(root), `${root}.lwsowner`);
21+
});
22+
23+
it('readOwners returns [] for missing/corrupt sidecars', async () => {
24+
assert.deepEqual(await readOwners(storage, '/nowhere/'), []);
25+
await storage.write(ownerStorePath(root), Buffer.from('{corrupt'));
26+
assert.deepEqual(await readOwners(storage, root), []);
27+
await writeOwners(storage, root, [pod.webId]); // restore
28+
});
29+
30+
it('ensureDeclaredType merges, never overwrites, idempotent', async () => {
31+
await storage.write(typeStorePath(root), Buffer.from(JSON.stringify(['https://example.org/Custom'])));
32+
assert.equal(await ensureDeclaredType(storage, root, LWS_STORAGE), true);
33+
const types = await readDeclaredTypes(storage, root);
34+
assert.ok(types.includes('https://example.org/Custom')); // merge, not overwrite
35+
assert.ok(types.includes(LWS_STORAGE));
36+
assert.equal(await ensureDeclaredType(storage, root, LWS_STORAGE), false); // idempotent
37+
});
38+
});

0 commit comments

Comments
 (0)