@inerrata-corporation/errata 2.0.0-dev.76 → 2.0.0-dev.78

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.
Files changed (2) hide show
  1. package/errata.mjs +158 -3
  2. package/package.json +1 -1
package/errata.mjs CHANGED
@@ -20899,6 +20899,11 @@ function toWirePayload(batch, runId) {
20899
20899
  // Origin key (= project) for the refute channel's cross-origin gate; absent
20900
20900
  // for id-less batches (principle sync) → those nodes stay fail-open.
20901
20901
  ...batch.originProject ? { originProject: batch.originProject } : {},
20902
+ // PJ-write (§5a): the AUDIENCE assertion — which project stratum this batch is
20903
+ // contributed to. Verified server-side against the projected link record; an
20904
+ // unverifiable assertion rejects the whole batch rather than downgrading it to a
20905
+ // wider audience. Absent ⇒ exactly today's wire.
20906
+ ...batch.projectId ? { projectId: batch.projectId } : {},
20902
20907
  nodes,
20903
20908
  edges,
20904
20909
  ...symbolSummaries ? { symbolSummaries } : {}
@@ -21145,6 +21150,18 @@ var init_client = __esm({
21145
21150
  async attachProject(projectId, locator) {
21146
21151
  return this.json("POST", "/api/gate/projects/attach", { projectId, locator });
21147
21152
  }
21153
+ /** Fetch the project's symbol salt (PJ-anchors · plan §6c) — the HMAC key this
21154
+ * workspace needs to derive project-canonical `sym_` anchor ids that CONVERGE
21155
+ * with every other member's.
21156
+ *
21157
+ * Members-only: the server gates it with the same fail-closed check the ingest
21158
+ * door uses (known + active + owned by the caller's org), and 403s otherwise.
21159
+ * Safe to hold locally for exactly one reason — a project member can already read
21160
+ * the project's code, so the salt lets them compute ids for symbols they can see
21161
+ * anyway. It is key material: keep it in memory, never log it. */
21162
+ async projectSymbolSalt(projectId) {
21163
+ return this.json("GET", `/v2/projects/${encodeURIComponent(projectId)}/symbol-salt`);
21164
+ }
21148
21165
  /** Create a locator-less project — `errata link --name` for repos without a
21149
21166
  * usable remote. Discovery can't find it; the caller stamps the id locally. */
21150
21167
  async createProject(name2) {
@@ -21336,6 +21353,16 @@ var init_client = __esm({
21336
21353
  if (q.seed?.length) qs.set("seed", q.seed.join(","));
21337
21354
  if (q.q) qs.set("q", q.q);
21338
21355
  const res = await this.json("GET", `/v2/search?${qs.toString()}`);
21356
+ const { nodes, edges } = this.toCastalia(res);
21357
+ return { nodes, edges, generatedAt: res.generatedAt };
21358
+ }
21359
+ /**
21360
+ * The castalia wire shape → local-mergeable graph. Extracted so the priming fetch
21361
+ * (`getProjectPriors`) lands nodes through the EXACT same mapper as `search`: a
21362
+ * second mapper would be a second convergence-key convention to keep in sync, and
21363
+ * an id-keying drift between the two is a silent no-op, not a crash.
21364
+ */
21365
+ toCastalia(res) {
21339
21366
  const now = Date.now();
21340
21367
  const nodes = res.nodes.map((n) => ({
21341
21368
  id: n.canonicalId,
@@ -21369,7 +21396,25 @@ var init_client = __esm({
21369
21396
  navFailures: 0,
21370
21397
  attrs: { ...e.attrs, source: "cloud" }
21371
21398
  }));
21372
- return { nodes, edges, generatedAt: res.generatedAt };
21399
+ return { nodes, edges };
21400
+ }
21401
+ /**
21402
+ * PJ-hydrate: the one-time seeded prime a fresh clone pulls at attach.
21403
+ *
21404
+ * A clone can already READ its project stratum (query-time federation, PJ-read) —
21405
+ * what it can't do is PRIME, because the passive block builds from the local store
21406
+ * and the remote blend is seeded from local problems a new checkout doesn't have.
21407
+ * This lands the top of the stratum locally so both have something to stand on.
21408
+ *
21409
+ * Membership-gated server-side by the same fail-closed check as ingest: a caller
21410
+ * who couldn't write this project's stratum can't prime from it.
21411
+ */
21412
+ async getProjectPriors(projectId, limit = 50) {
21413
+ const res = await this.json(
21414
+ "GET",
21415
+ `/v2/projects/${encodeURIComponent(projectId)}/priors?limit=${String(limit)}`
21416
+ );
21417
+ return this.toCastalia(res);
21373
21418
  }
21374
21419
  /** Pull cloud-induced skills + the session-bootstrap prime payload. `seed` is the
21375
21420
  * canonical ids of the agent's recent problems — the cloud ranks the skills whose
@@ -43276,7 +43321,7 @@ function startLoopLagMonitor(thresholdMs = 1e3) {
43276
43321
  }
43277
43322
 
43278
43323
  // src/engine.ts
43279
- var DAEMON_VERSION = true ? "2.0.0-dev.76" : "2.0.0-alpha.0";
43324
+ var DAEMON_VERSION = true ? "2.0.0-dev.78" : "2.0.0-alpha.0";
43280
43325
  var IGNORED_PATH = /[\\/](?:\.git|node_modules|\.errata|\.claude|\.codex|\.cursor|\.turbo|\.next|dist|coverage|test-results|playwright-report|__pycache__)(?:[\\/]|$)/;
43281
43326
  var IGNORED_NOISE = /castalia\.db|eventlog\.sqlite|turn-cursors/;
43282
43327
  var GIT_OP_MUTE_MS = 4e3;
@@ -45005,6 +45050,7 @@ function runLockfilePass(opts) {
45005
45050
  init_src();
45006
45051
  init_src2();
45007
45052
  init_src8();
45053
+ init_src();
45008
45054
 
45009
45055
  // src/contribution-ready.ts
45010
45056
  init_src3();
@@ -45143,6 +45189,50 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
45143
45189
  contextNodes.push(shareableContext(t, wireId));
45144
45190
  }
45145
45191
  }
45192
+ const project = opts.project;
45193
+ if (project) {
45194
+ const now = Date.now();
45195
+ for (const sourceId of anchorSources) {
45196
+ const source = store.getNode(sourceId);
45197
+ if (!source) continue;
45198
+ for (const e of store.outEdges(sourceId, ["ANCHORED_AT"])) {
45199
+ const target = store.getNode(e.to);
45200
+ if (!target) continue;
45201
+ const relPath = target.attrs["relPath"];
45202
+ const qname = target.attrs["qname"];
45203
+ if (typeof relPath !== "string" || typeof qname !== "string") continue;
45204
+ if (ignored(relPath) || ignored(qname)) continue;
45205
+ const symId = projectSymbolId(project.salt, project.projectId, relPath, qname, target.label);
45206
+ if (!contextSeen.has(symId)) {
45207
+ contextSeen.add(symId);
45208
+ contextNodes.push({
45209
+ id: symId,
45210
+ label: "Symbol",
45211
+ // The id IS the description: there is nothing that can be said about an
45212
+ // opaque symbol without disclosing it, and the wire requires a non-empty
45213
+ // string. It must never carry the name or the path.
45214
+ description: symId,
45215
+ // `kind` is coarse (Function/Class/…) — shape, not identity.
45216
+ attrs: { kind: target.label },
45217
+ embedding: [],
45218
+ extractionConfidence: 1,
45219
+ extractionSource: "daemon-extracted",
45220
+ cumulativeSurprise: 0,
45221
+ peakSurprise: 0,
45222
+ cumulativeHits: 0,
45223
+ lastUpdatedAt: now,
45224
+ createdAt: now,
45225
+ memoryTier: "short-term",
45226
+ pageRank: 0,
45227
+ isLandmark: false,
45228
+ community: null,
45229
+ stability: "unstable"
45230
+ });
45231
+ }
45232
+ anchorEdges.push({ ...e, to: symId, attrs: {} });
45233
+ }
45234
+ }
45235
+ }
45146
45236
  if (nodes.length === 0 && anchorEdges.length === 0) return null;
45147
45237
  let symbolSummaries;
45148
45238
  if (opts.lexicon && opts.lexicon.size > 0 && nodes.length > 0) {
@@ -45401,6 +45491,39 @@ async function ensureProjectLink(root, profile, client) {
45401
45491
  }
45402
45492
  }
45403
45493
 
45494
+ // src/project-hydrate.ts
45495
+ var DEFAULT_LIMIT = 50;
45496
+ async function hydrateProject(opts) {
45497
+ const { root, profile, store, cloud } = opts;
45498
+ if (!profile.projectId) return { hydrated: false, reason: "no-project" };
45499
+ if (profile.projectHydratedAt) return { hydrated: false, reason: "already-hydrated" };
45500
+ try {
45501
+ const res = await cloud.getProjectPriors(profile.projectId, opts.limit ?? DEFAULT_LIMIT);
45502
+ let landedNodes = 0;
45503
+ for (const n of res.nodes) {
45504
+ if (store.getNode(n.id)) continue;
45505
+ store.mergeNode(n);
45506
+ landedNodes++;
45507
+ }
45508
+ let landedEdges = 0;
45509
+ for (const e of res.edges) {
45510
+ if (!store.getNode(e.from) || !store.getNode(e.to)) continue;
45511
+ if (store.getEdge(e.id)) continue;
45512
+ store.mergeEdge(e);
45513
+ landedEdges++;
45514
+ }
45515
+ profile.projectHydratedAt = Date.now();
45516
+ saveProfile(root, profile);
45517
+ return { hydrated: true, nodes: landedNodes, edges: landedEdges };
45518
+ } catch (err2) {
45519
+ return {
45520
+ hydrated: false,
45521
+ reason: "error",
45522
+ detail: err2 instanceof Error ? err2.message : String(err2)
45523
+ };
45524
+ }
45525
+ }
45526
+
45404
45527
  // src/adopt.ts
45405
45528
  import { existsSync as existsSync22 } from "node:fs";
45406
45529
  import { dirname as dirname8, join as join25 } from "node:path";
@@ -45444,6 +45567,14 @@ function createRootAdopter(deps) {
45444
45567
  }
45445
45568
 
45446
45569
  // src/multi.ts
45570
+ var projectSymbolSalts = /* @__PURE__ */ new Map();
45571
+ async function resolveProjectSymbolSalt(client, projectId) {
45572
+ const cached2 = projectSymbolSalts.get(projectId);
45573
+ if (cached2) return { projectId, salt: cached2 };
45574
+ const { salt } = await client.projectSymbolSalt(projectId);
45575
+ projectSymbolSalts.set(projectId, salt);
45576
+ return { projectId, salt };
45577
+ }
45447
45578
  function normPath(p) {
45448
45579
  return p.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
45449
45580
  }
@@ -45834,6 +45965,20 @@ async function startMultiDaemon(opts = {}) {
45834
45965
  } else if (out2.reason === "error") {
45835
45966
  console.warn(`[errata] project link failed for ${r.engine.profile.name}: ${out2.detail}`);
45836
45967
  }
45968
+ const hyd = await hydrateProject({
45969
+ root: r.root,
45970
+ profile: r.engine.profile,
45971
+ store: r.engine.store,
45972
+ cloud: client
45973
+ });
45974
+ if (hyd.hydrated) {
45975
+ r.engine.markContextDirty();
45976
+ console.log(
45977
+ `[errata] primed ${r.engine.profile.name} from project \u2014 ${hyd.nodes} priors, ${hyd.edges} links`
45978
+ );
45979
+ } else if (hyd.reason === "error") {
45980
+ console.warn(`[errata] project prime failed for ${r.engine.profile.name}: ${hyd.detail}`);
45981
+ }
45837
45982
  }
45838
45983
  };
45839
45984
  void ambientLinkAll();
@@ -46052,11 +46197,21 @@ async function startMultiDaemon(opts = {}) {
46052
46197
  if (summaries.size > 0) lexicon = buildSymbolLexicon(store, summaries);
46053
46198
  } catch {
46054
46199
  }
46200
+ const projectId = r.engine.profile.projectId;
46201
+ let project;
46202
+ if (projectId) {
46203
+ try {
46204
+ project = await resolveProjectSymbolSalt(client, projectId);
46205
+ } catch {
46206
+ }
46207
+ }
46055
46208
  const instances = buildInstanceIngest(store, r.engine.profile, DAEMON_VERSION, ignore, {
46056
46209
  includePackages: cfg2.consent.contributePackages,
46057
- ...lexicon ? { lexicon } : {}
46210
+ ...lexicon ? { lexicon } : {},
46211
+ ...project ? { project } : {}
46058
46212
  });
46059
46213
  if (instances) {
46214
+ if (project) instances.projectId = project.projectId;
46060
46215
  const res = await client.ingest(instances);
46061
46216
  uploaded += res.accepted;
46062
46217
  const seq = store.currentIngestSeq();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inerrata-corporation/errata",
3
- "version": "2.0.0-dev.76",
3
+ "version": "2.0.0-dev.78",
4
4
  "description": "errata - local-first observation engine for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {