@kungfu-tech/buildchain 4.1.3-alpha.0 → 4.1.3-alpha.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.
Files changed (34) hide show
  1. package/architecture/agent-change-map.md +6 -2
  2. package/architecture/internal-capabilities.json +9 -2
  3. package/architecture/maintainability-policy.json +3 -3
  4. package/architecture/release-topology.json +4 -1
  5. package/architecture/universal-workflow-capability-policy.json +5 -5
  6. package/contracts/release-discussion-v1.schema.json +3 -0
  7. package/dist/readers/release-discussion.cjs +4 -0
  8. package/dist/site/buildchain-contract.json +4 -4
  9. package/dist/site/buildchain-site.json +14 -9
  10. package/dist/site/kfd-claims.json +2 -2
  11. package/dist/site/kfd-upstream-aggregate.json +1 -1
  12. package/dist/site/manual-registry.json +1 -1
  13. package/dist/site/node-api-registry.json +3 -3
  14. package/dist/site/page-registry.json +9 -4
  15. package/dist/site/public-surface-audit.json +2 -2
  16. package/dist/site/publication-registry.json +4 -4
  17. package/dist/site/site-manifest.json +5 -5
  18. package/dist/site/workflow-registry.json +4 -4
  19. package/docs/node-api-reference.md +1 -1
  20. package/docs/release-discussions.md +44 -5
  21. package/package.json +1 -1
  22. package/packages/core/providers/github/discussions/materials.js +16 -5
  23. package/packages/core/providers/github/discussions/transport.js +16 -5
  24. package/packages/core/release/discussion/binary.js +11 -0
  25. package/packages/core/release/discussion/checkpoints.js +45 -4
  26. package/packages/core/release/discussion/envelope.js +1 -0
  27. package/packages/core/release/discussion/evidence.js +51 -0
  28. package/packages/core/release/discussion/presentation.js +92 -0
  29. package/packages/core/release/discussion/publication.js +3 -1
  30. package/packages/core/release/discussion/qualification.js +1 -0
  31. package/packages/core/release/discussion/reader.js +9 -0
  32. package/packages/core/release/discussion/session.js +2 -0
  33. package/packages/core/release/discussion/store.js +28 -31
  34. package/packages/core/release/discussion/threads.js +83 -0
@@ -1,3 +1,5 @@
1
+ import { readDiscussionThreads, eventParent } from "./threads.js";
2
+ import { renderIntent, renderEvent } from "./presentation.js";
1
3
  import { collectDiscussionPages } from "../../providers/github/discussions/transport.js";
2
4
  import {
3
5
  canonicalJson,
@@ -95,11 +97,10 @@ export function releaseDiscussionStore(
95
97
  discussion = await transport.create({
96
98
  repositoryId: repository.id,
97
99
  categoryId: selected[0].id,
98
- title: `Buildchain release: ${intent.key}`,
99
- body: encodeRecord(
100
- intent,
101
- `Release transaction for ${intent.key}. Expected nodes: ${intent.expectedNodes.join(", ")}.`,
102
- ),
100
+ title: intent.source?.qualification
101
+ ? `Buildchain qualification: ${intent.source.qualification}`
102
+ : `Buildchain release: ${intent.key}`,
103
+ body: encodeRecord(intent, renderIntent(intent)),
103
104
  });
104
105
  } catch (error) {
105
106
  discussion = await observeAfterUnknown(find, error);
@@ -110,39 +111,30 @@ export function releaseDiscussionStore(
110
111
  }
111
112
  async function read(session) {
112
113
  const discussion = await transport.get(session.discussion.id);
113
- assertDiscussion(discussion, session.intent, session.writerId);
114
- const comments = await collectDiscussionPages((after) =>
115
- transport.comments(discussion.id, after),
116
- );
117
- const trusted = comments.filter(
118
- (comment) => comment.author?.id === session.writerId,
114
+ const intent = assertDiscussion(
115
+ discussion,
116
+ session.intent,
117
+ session.writerId,
119
118
  );
120
- if (
121
- trusted.some(
122
- (comment) => comment.lastEditedAt && decodeRecord(comment.body),
123
- )
124
- )
125
- throw new Error(
126
- "A transaction record was edited; historical facts are no longer intact",
127
- );
128
- const records = trusted
129
- .map((comment) => decodeRecord(comment.body))
130
- .filter(Boolean);
119
+ const threads = await readDiscussionThreads(transport, {
120
+ ...session,
121
+ intent,
122
+ discussion,
123
+ });
131
124
  return {
132
125
  discussion,
133
- comments: trusted,
134
- records,
135
- ...readReleaseDiscussion({ body: discussion.body, records }),
126
+ ...threads,
127
+ ...readReleaseDiscussion({
128
+ body: discussion.body,
129
+ records: threads.records,
130
+ }),
136
131
  };
137
132
  }
138
133
  async function append(session, record) {
139
134
  if (session.dryRun) return { dryRun: true };
140
- const body = encodeRecord(
141
- record,
142
- `${record.node}: ${record.status} (attempt ${record.attempt})`,
143
- );
135
+ let observed;
144
136
  const find = async () => {
145
- const observed = await read(session);
137
+ observed = await read(session);
146
138
  const existing = observed.comments.find(
147
139
  (comment) => decodeRecord(comment.body)?.id === record.id,
148
140
  );
@@ -155,8 +147,13 @@ export function releaseDiscussionStore(
155
147
  };
156
148
  const existing = await find();
157
149
  if (existing) return existing;
150
+ const parent = eventParent(observed, record);
151
+ const body = encodeRecord(
152
+ record,
153
+ renderEvent(record, session.intent, observed),
154
+ );
158
155
  try {
159
- await transport.append(session.discussion.id, body);
156
+ await transport.append(session.discussion.id, body, parent);
160
157
  } catch (error) {
161
158
  return observeAfterUnknown(find, error);
162
159
  }
@@ -0,0 +1,83 @@
1
+ import { decodeRecord } from "./envelope.js";
2
+ import { collectDiscussionPages } from "../../providers/github/discussions/transport.js";
3
+
4
+ export function isAttemptRoot(record) {
5
+ return (
6
+ record?.node === "attempt" &&
7
+ record.kind === "progress" &&
8
+ record.status === "running" &&
9
+ record.sequence === 0
10
+ );
11
+ }
12
+
13
+ // Provider placement is checked before the pure reader reduces event semantics.
14
+ // Historical flat records retain their original representation; new writes always
15
+ // use roots and replies. An explicit intent organization makes placement strict.
16
+ export async function readDiscussionThreads(transport, session) {
17
+ const top = await collectDiscussionPages((after) =>
18
+ transport.comments(session.discussion.id, after),
19
+ );
20
+ const comments = [...top];
21
+ for (const root of top) {
22
+ if (root.replies?.totalCount === 0) continue;
23
+ const replies = await collectDiscussionPages((after) =>
24
+ transport.replies(root.id, after),
25
+ );
26
+ for (const reply of replies) {
27
+ if (reply.replyTo?.id !== root.id)
28
+ throw new Error(
29
+ "Discussion reply parent differs from its queried thread",
30
+ );
31
+ }
32
+ comments.push(...replies);
33
+ if (Buffer.byteLength(JSON.stringify(comments)) > 32 * 1024 * 1024)
34
+ throw new Error("Discussion threads exceed the complete-view byte bound");
35
+ }
36
+ const trusted = comments.filter(
37
+ (comment) => comment.author?.id === session.writerId,
38
+ );
39
+ const records = new Map();
40
+ for (const comment of trusted) {
41
+ const record = decodeRecord(comment.body);
42
+ if (!record) continue;
43
+ if (comment.lastEditedAt)
44
+ throw new Error(
45
+ "A transaction record was edited; historical facts are no longer intact",
46
+ );
47
+ records.set(comment.id, record);
48
+ }
49
+ const roots = new Map();
50
+ for (const comment of trusted) {
51
+ const record = records.get(comment.id);
52
+ if (!isAttemptRoot(record)) continue;
53
+ if (comment.replyTo || roots.has(record.attempt))
54
+ throw new Error("Ambiguous or nested Discussion attempt root");
55
+ roots.set(record.attempt, comment);
56
+ }
57
+ for (const comment of trusted) {
58
+ const record = records.get(comment.id);
59
+ if (!record || isAttemptRoot(record)) continue;
60
+ const root = roots.get(record.attempt);
61
+ if (comment.replyTo) {
62
+ if (!root || comment.replyTo.id !== root.id)
63
+ throw new Error(
64
+ "Transaction event is attached to a different attempt root",
65
+ );
66
+ } else if (session.intent.organization === "attempt-threads/v1") {
67
+ throw new Error("Transaction event requires its attempt root reply");
68
+ }
69
+ }
70
+ return { comments: trusted, records: [...records.values()], roots };
71
+ }
72
+
73
+ export function eventParent(state, record) {
74
+ const root = state.roots.get(record.attempt);
75
+ if (isAttemptRoot(record)) {
76
+ if (root)
77
+ throw new Error("Attempt root already exists with different content");
78
+ return null;
79
+ }
80
+ if (!root)
81
+ throw new Error("Open the attempt root before appending its events");
82
+ return root.id;
83
+ }