@mean-weasel/lineage 0.1.4 → 0.1.5

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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.5
4
+
5
+ - Add graph orientation controls for lineage views and CLI flows, including browser coverage for orientation behavior.
6
+ - Improve agent claim visibility with workspace/content occupancy badges, claim lifecycle controls, and release claim smoke coverage.
7
+ - Harden lineage claim enforcement for explicit child workspaces and `project_channel` claims so scoped claims cannot authorize broader writes.
8
+ - Make local startup helpers durable with tmux-backed Makefile commands.
9
+
3
10
  ## 0.1.4
4
11
 
5
12
  - Implement target-scoped agent claims for lineage and content-post agent writes, including claim lifecycle CLI/API commands, heartbeat/release/revoke/transfer controls, and token-redacted read APIs.
package/README.md CHANGED
@@ -54,6 +54,12 @@ lineage agent claim --project demo-project --scope lineage_workspace --target de
54
54
  export LINEAGE_CLAIM_TOKEN='claim_abc.secret_xyz'
55
55
  ```
56
56
 
57
+ Agents can inspect the current lineage graph without a claim:
58
+
59
+ ```bash
60
+ lineage agent graph --project demo-project --root <root-asset-id> --json
61
+ ```
62
+
57
63
  Keep the claim fresh and pass it to mutating commands:
58
64
 
59
65
  ```bash
@@ -127,12 +133,13 @@ For local release validation:
127
133
 
128
134
  ```bash
129
135
  npm run release:dry-run -- --tag next
136
+ npm run release:claim-smoke -- --package @mean-weasel/lineage@next
130
137
  npm run release:next
131
138
  npm run release:dry-run -- --tag latest
132
139
  npm run release:latest
133
140
  ```
134
141
 
135
- The release script verifies package metadata, changelog version coverage, public-readiness scans, install smoke, browser smoke, audit, and package contents before publishing. GitHub Actions runs CI on pull requests and `main`; publishing is manual through the Release workflow.
142
+ The release script verifies package metadata, changelog version coverage, public-readiness scans, install smoke, browser smoke, audit, and package contents before publishing. Promotion also installs the candidate package and runs a claim lifecycle smoke that creates a target claim, proves missing-token writes fail, proves matching-token writes succeed, verifies read surfaces do not expose the raw token, and proves release invalidates the token. GitHub Actions runs CI on pull requests and `main`; publishing is manual through the Release workflow.
136
143
 
137
144
  Use the Release workflow operations this way:
138
145
 
@@ -1297,6 +1297,37 @@ function activeLineageWorkspaceRoot(project) {
1297
1297
  }
1298
1298
  }
1299
1299
 
1300
+ // src/server/lineageClaimGuards.ts
1301
+ function channelOverlaps2(left, right) {
1302
+ return !left || !right || left === right;
1303
+ }
1304
+ function hasActiveLineageWorkspaceClaim(project, targetId, channel) {
1305
+ return listAgentClaims(project).claims.some((claim) => {
1306
+ if (claim.project !== project || claim.status !== "active" || claim.derived_state === "expired") return false;
1307
+ if (claim.scope_type === "lineage_workspace") return claim.target_id === targetId;
1308
+ return claim.scope_type === "project_channel" && channelOverlaps2(channel, claim.channel);
1309
+ });
1310
+ }
1311
+ function requireLineageWorkspaceClaimForWrite(fields) {
1312
+ if (!fields.confirmWrite) return;
1313
+ const targetId = lineageWorkspaceId(fields.project, fields.rootAssetId);
1314
+ if (!fields.claimToken && !hasActiveLineageWorkspaceClaim(fields.project, targetId, fields.channel)) return;
1315
+ const validation = validateAgentClaimForWrite({
1316
+ channel: fields.channel,
1317
+ claimToken: fields.claimToken,
1318
+ confirmWrite: fields.confirmWrite,
1319
+ dangerLevel: "enforce",
1320
+ project: fields.project,
1321
+ scopeType: "lineage_workspace",
1322
+ targetId,
1323
+ writeKind: fields.writeKind
1324
+ });
1325
+ if (!validation.ok) {
1326
+ const status = validation.code === "claim_required" || validation.code === "claim_token_invalid" ? 401 : 409;
1327
+ throw new AgentClaimError(validation.message, status, validation.code, validation.conflicts);
1328
+ }
1329
+ }
1330
+
1300
1331
  // src/server/assetLineage.ts
1301
1332
  var LineageError = class extends Error {
1302
1333
  constructor(message, status = 400) {
@@ -1324,6 +1355,45 @@ function rootFor(database, project, assetId) {
1324
1355
  }
1325
1356
  return assetId;
1326
1357
  }
1358
+ function explicitWorkspaceRoot(database, project, assetId) {
1359
+ const row = database.prepare(`
1360
+ select root_asset_id from lineage_workspaces
1361
+ where project_id = ? and root_asset_id = ? and status != 'archived'
1362
+ `).get(project, assetId);
1363
+ return row?.root_asset_id;
1364
+ }
1365
+ function nearestWorkspaceRoot(database, project, assetId) {
1366
+ let current = assetId;
1367
+ const seen = /* @__PURE__ */ new Set();
1368
+ while (!seen.has(current)) {
1369
+ seen.add(current);
1370
+ const explicit = explicitWorkspaceRoot(database, project, current);
1371
+ if (explicit) return explicit;
1372
+ const parent = parentOf(database, project, current);
1373
+ if (!parent) return current;
1374
+ current = parent;
1375
+ }
1376
+ return assetId;
1377
+ }
1378
+ function assetChannel(database, project, assetId) {
1379
+ const row = database.prepare("select channel from assets where project_id = ? and id = ?").get(project, assetId);
1380
+ return row?.channel;
1381
+ }
1382
+ function lineageWriteClaimContext(database, project, assetId) {
1383
+ return {
1384
+ channel: assetChannel(database, project, assetId),
1385
+ rootAssetId: nearestWorkspaceRoot(database, project, assetId)
1386
+ };
1387
+ }
1388
+ function getLineageWriteClaimContext(project, assetId) {
1389
+ const database = lineageDb();
1390
+ try {
1391
+ requireAsset(database, project, assetId);
1392
+ return lineageWriteClaimContext(database, project, assetId);
1393
+ } finally {
1394
+ database.close();
1395
+ }
1396
+ }
1327
1397
  function latestSelectedRoot(database, project) {
1328
1398
  const row = database.prepare("select root_asset_id from asset_selections where project_id = ? order by selected_at desc limit 1").get(project);
1329
1399
  return row?.root_asset_id;
@@ -1354,6 +1424,20 @@ function linkLineageAssets(project, fields) {
1354
1424
  requireAsset(database, project, fields.parentAssetId);
1355
1425
  requireAsset(database, project, fields.childAssetId);
1356
1426
  if (fields.parentAssetId === fields.childAssetId) throw new LineageError("Lineage link cannot point to itself");
1427
+ const claimContext = lineageWriteClaimContext(database, project, fields.parentAssetId);
1428
+ try {
1429
+ requireLineageWorkspaceClaimForWrite({
1430
+ channel: claimContext.channel,
1431
+ claimToken: fields.claimToken,
1432
+ confirmWrite: fields.confirmWrite,
1433
+ project,
1434
+ rootAssetId: claimContext.rootAssetId,
1435
+ writeKind: "lineage_link"
1436
+ });
1437
+ } catch (error) {
1438
+ database.close();
1439
+ throw error;
1440
+ }
1357
1441
  const edge = {
1358
1442
  id: edgeId(project, fields.parentAssetId, fields.childAssetId),
1359
1443
  parent_asset_id: fields.parentAssetId,
@@ -1390,7 +1474,7 @@ function descendants(database, project, root) {
1390
1474
  function getLineageSnapshot(project, assetId) {
1391
1475
  const database = lineageDb();
1392
1476
  requireAsset(database, project, assetId);
1393
- const root = rootFor(database, project, assetId);
1477
+ const root = explicitWorkspaceRoot(database, project, assetId) || rootFor(database, project, assetId);
1394
1478
  const edges = descendants(database, project, root);
1395
1479
  const ids = [.../* @__PURE__ */ new Set([root, ...edges.flatMap((edge) => [edge.parent_asset_id, edge.child_asset_id])])];
1396
1480
  const placeholders = ids.map(() => "?").join(",");
@@ -1566,14 +1650,15 @@ function linkSelectedLineageChild(project, fields) {
1566
1650
  const next = getLineageNextAsset(project, fields.rootAssetId);
1567
1651
  if (!next.next_asset) throw new LineageError("Cannot link child until a next base is selected or unambiguous");
1568
1652
  if (fields.confirmWrite) {
1653
+ const claimContext = getLineageWriteClaimContext(project, next.next_asset.asset_id);
1569
1654
  const validation = validateAgentClaimForWrite({
1570
- channel: next.next_asset.channel,
1655
+ channel: claimContext.channel,
1571
1656
  claimToken: fields.claimToken,
1572
1657
  confirmWrite: fields.confirmWrite,
1573
1658
  dangerLevel: "enforce",
1574
1659
  project,
1575
1660
  scopeType: "lineage_workspace",
1576
- targetId: lineageWorkspaceId(project, next.root_asset_id),
1661
+ targetId: lineageWorkspaceId(project, claimContext.rootAssetId),
1577
1662
  writeKind: "link_child"
1578
1663
  });
1579
1664
  if (!validation.ok) throw new AgentClaimError(validation.message, validation.code === "claim_required" ? 401 : 403, validation.code, validation.conflicts);
@@ -1649,6 +1734,7 @@ Usage:
1649
1734
  ${config.binName} inspect --asset-id <asset-id> [--project <project>] [--db <path>] [--json]
1650
1735
  ${config.binName} link-child --root <asset-id> --child <asset-id> [--project <project>] [--claim-token <claim-id.secret>] [--confirm-write] [--db <path>] [--json]
1651
1736
  ${config.binName} agent claim --project <project> --scope <scope> --target <target-id> --agent-name <name> [--channel <channel>] [--ttl 20m] [--json]
1737
+ ${config.binName} agent graph --root <asset-id> [--project <project>] [--db <path>] [--json]
1652
1738
  ${config.binName} agent status [--project <project>] [--json]
1653
1739
  ${config.binName} agent inspect --claim <claim-id> [--project <project>] [--json]
1654
1740
  ${config.binName} agent heartbeat --claim-token <claim-id.secret> [--json]
@@ -1765,6 +1851,11 @@ function runLineageAgentCommand(command, args) {
1765
1851
  });
1766
1852
  }
1767
1853
  if (command === "status") return listAgentClaims(project);
1854
+ if (command === "graph") {
1855
+ const rootAssetId = readOption(args, "--root") || readOption(args, "--asset-id") || positionalArgs(args)[0];
1856
+ if (!rootAssetId) throw new Error("lineage agent graph requires --root");
1857
+ return getLineageSnapshot(project, rootAssetId);
1858
+ }
1768
1859
  if (command === "inspect") {
1769
1860
  if (!claimId) throw new Error("lineage agent inspect requires --claim");
1770
1861
  return inspectAgentClaim(claimId, project);
@@ -1821,8 +1912,37 @@ function printAgentResult(command, result, json) {
1821
1912
  console.log(`${inspected.claim?.id || "claim"} ${inspected.claim?.status || "unknown"} ${inspected.claim?.target_id || ""}`.trim());
1822
1913
  return;
1823
1914
  }
1915
+ if (command === "graph" && result && typeof result === "object" && "nodes" in result) {
1916
+ console.log(formatAgentGraphDigest(result));
1917
+ return;
1918
+ }
1824
1919
  console.log(String(result));
1825
1920
  }
1921
+ function formatAgentGraphDigest(snapshot) {
1922
+ const lines = [];
1923
+ const titleFor = (assetId) => {
1924
+ const node = snapshot.nodes.find((item) => item.asset_id === assetId);
1925
+ return node?.title ? `${node.title} (${assetId})` : assetId;
1926
+ };
1927
+ const root = snapshot.nodes.find((node) => node.asset_id === snapshot.root_asset_id);
1928
+ lines.push(`Lineage graph: ${root?.title || snapshot.root_asset_id}`);
1929
+ lines.push(`Root: ${snapshot.root_asset_id}`);
1930
+ lines.push(`Active: ${titleFor(snapshot.active_asset_id)}`);
1931
+ lines.push(`Nodes: ${snapshot.nodes.length} Edges: ${snapshot.edges.length}`);
1932
+ const selected = snapshot.selected || [];
1933
+ if (selected.length > 0) {
1934
+ lines.push("Next variation:");
1935
+ for (const assetId of selected) lines.push(`- ${titleFor(assetId)}`);
1936
+ }
1937
+ const latest = snapshot.latest || snapshot.nodes.filter((node) => node.is_latest).map((node) => node.asset_id);
1938
+ if (latest.length > 0) {
1939
+ lines.push("Latest leaves:");
1940
+ for (const assetId of latest) lines.push(`- ${titleFor(assetId)}`);
1941
+ }
1942
+ lines.push("Edges:");
1943
+ for (const edge of snapshot.edges) lines.push(`- ${titleFor(edge.parent_asset_id)} -> ${titleFor(edge.child_asset_id)}`);
1944
+ return lines.join("\n");
1945
+ }
1826
1946
  function start(config, args) {
1827
1947
  let options;
1828
1948
  const json = args.includes("--json");