@cleocode/contracts 2026.9.21 → 2026.9.22

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.
@@ -499,6 +499,28 @@ describe('parseEvidenceString (T10337)', () => {
499
499
 
500
500
  // ─── GATE_EVIDENCE_REQUIREMENTS — gate-to-atom mapping ─────────────────────
501
501
 
502
+ describe('pr:/ci: component references (T12671)', () => {
503
+ it('pr:<component>@<integration> names the integration PR and its component', () => {
504
+ expect(parseEvidenceString('pr:42@41')).toEqual([
505
+ { kind: 'pr', prNumber: 41, componentPrNumber: 42 },
506
+ ]);
507
+ expect(parseEvidenceString('ci:42@41')).toEqual([
508
+ { kind: 'ci', prNumber: 41, componentPrNumber: 42 },
509
+ ]);
510
+ expect(parseEvidenceString('pr:41')).toEqual([{ kind: 'pr', prNumber: 41 }]);
511
+ });
512
+
513
+ it.each(['pr:42@42', 'pr:42@', 'pr:@41', 'ci:0@41', 'pr:42@4x'])('rejects %s', (raw) => {
514
+ expect(() => parseEvidenceString(raw)).toThrow(EvidenceParseError);
515
+ });
516
+
517
+ it('the schema accepts the component field', () => {
518
+ expect(
519
+ EvidenceAtomSchema.safeParse({ kind: 'ci', prNumber: 41, componentPrNumber: 42 }).success,
520
+ ).toBe(true);
521
+ });
522
+ });
523
+
502
524
  describe('GATE_EVIDENCE_REQUIREMENTS (T10337)', () => {
503
525
  it('covers every VerificationGate', () => {
504
526
  const expected: VerificationGate[] = [
@@ -8456,6 +8456,7 @@ export const OPERATIONS: OperationDef[] = [
8456
8456
  enum: DOCS_LIFECYCLE_STATUSES,
8457
8457
  description:
8458
8458
  'Override the new lifecycle status. Defaults to "draft" on every update. ' +
8459
+ 'Alone (no file or content) it changes only the lifecycle status. ' +
8459
8460
  `Valid: ${DOCS_LIFECYCLE_STATUSES.join('|')}.`,
8460
8461
  },
8461
8462
  {
package/src/done-plan.ts CHANGED
@@ -119,6 +119,11 @@ export interface TaskChangeSet {
119
119
  executionRoot: string;
120
120
  /** Why {@link executionRoot} was chosen. */
121
121
  rootSource: ChangeSetRootSource;
122
+ /**
123
+ * Component PR whose change the (integration) `prNumber` landed on the
124
+ * default branch (T12671). Evidence is then `pr:<component>@<prNumber>`.
125
+ */
126
+ componentPrNumber?: number;
122
127
  /** Merged PR number (`source === 'pr'`). */
123
128
  prNumber?: number;
124
129
  /**
@@ -168,8 +173,23 @@ export interface TaskChangeSet {
168
173
  blockers: DonePlanBlocker[];
169
174
  /** Non-blocking observations, e.g. PR discovery that could not reach `gh`. */
170
175
  warnings: string[];
176
+ /**
177
+ * The merged-PR lookup failed (e.g. `gh` unavailable), so a non-PR change set
178
+ * does not prove the change is unmerged (T12656).
179
+ */
180
+ prDiscoveryFailed?: boolean;
181
+ /**
182
+ * Whether the task's PR has merged to the default branch, as the PR lookup
183
+ * found it — independent of `source` (T12656 review): a merged PR whose
184
+ * `pr:` check was refused still falls back to the task branch, and the
185
+ * branch outlives a squash merge. Absent when no PR was examined.
186
+ */
187
+ mergeState?: ChangeSetMergeState;
171
188
  }
172
189
 
190
+ /** Whether a task's change has merged to the default branch. */
191
+ export type ChangeSetMergeState = 'merged' | 'unmerged' | 'unknown';
192
+
173
193
  /** Planned state of one verification gate. */
174
194
  export interface DonePlanGate {
175
195
  /** Gate name. */
@@ -198,7 +198,11 @@ export const decisionAtomSchema = z.object({
198
198
  * checks task relationship and changed artifacts. Implementation also needs
199
199
  * inspected file evidence; testing and review require actual result atoms.
200
200
  *
201
- * Format: `pr:<positive integer>` (e.g. `pr:357`).
201
+ * Format: `pr:<positive integer>` (e.g. `pr:357`), or
202
+ * `pr:<component>@<integration>` (e.g. `pr:356@357`, T12671): component PR
203
+ * #356 merged into an integration branch that PR #357 landed on the default
204
+ * branch. The integration PR supplies the merge commit; the component PR
205
+ * supplies the task linkage and the changed files.
202
206
  *
203
207
  * @task T9764
204
208
  * @task T9838
@@ -206,6 +210,7 @@ export const decisionAtomSchema = z.object({
206
210
  export const prAtomSchema = z.object({
207
211
  kind: z.literal('pr'),
208
212
  prNumber: z.number().int().positive('pr atom requires a positive integer PR number'),
213
+ componentPrNumber: z.number().int().positive().optional(),
209
214
  });
210
215
 
211
216
  /**
@@ -214,13 +219,16 @@ export const prAtomSchema = z.object({
214
219
  * `qaPassed` only when the project opts in with `evidence.ciSatisfies`
215
220
  * (owner decision D11149).
216
221
  *
217
- * Format: `ci:<positive integer>` (e.g. `ci:357`).
222
+ * Format: `ci:<positive integer>` (e.g. `ci:357`), or
223
+ * `ci:<component>@<integration>` (e.g. `ci:356@357`, T12671): the integration
224
+ * PR's merge-commit CI, for a task linked through its component PR.
218
225
  *
219
226
  * @task T12634
220
227
  */
221
228
  export const ciAtomSchema = z.object({
222
229
  kind: z.literal('ci'),
223
230
  prNumber: z.number().int().positive('ci atom requires a positive integer PR number'),
231
+ componentPrNumber: z.number().int().positive().optional(),
224
232
  });
225
233
 
226
234
  /**
@@ -771,6 +779,21 @@ export class EvidenceParseError extends Error {
771
779
  * @task T10337
772
780
  * @adr ADR-051
773
781
  */
782
+ /**
783
+ * `<n>` or `<component>@<integration>` (T12671), each a positive integer.
784
+ * The integration PR is the atom's `prNumber`.
785
+ */
786
+ function parsePrReference(
787
+ payload: string,
788
+ ): { prNumber: number; componentPrNumber?: number } | null {
789
+ const match = /^(\d+)(?:@(\d+))?$/.exec(payload);
790
+ if (!match) return null;
791
+ const first = Number(match[1]);
792
+ const second = match[2] === undefined ? null : Number(match[2]);
793
+ if (first <= 0 || (second !== null && (second <= 0 || second === first))) return null;
794
+ return second === null ? { prNumber: first } : { prNumber: second, componentPrNumber: first };
795
+ }
796
+
774
797
  export function parseEvidenceString(raw: string): EvidenceAtom[] {
775
798
  if (!raw || typeof raw !== 'string') {
776
799
  throw new EvidenceParseError(
@@ -840,26 +863,16 @@ export function parseEvidenceString(raw: string): EvidenceAtom[] {
840
863
  atoms.push({ kind: 'decision', decisionId: payload });
841
864
  break;
842
865
  }
843
- case 'pr': {
844
- const prNumber = Number(payload);
845
- if (!Number.isInteger(prNumber) || prNumber <= 0) {
846
- throw new EvidenceParseError(
847
- `pr atom requires a positive integer PR number, got "${payload}" in "${chunk}"`,
848
- 'Use format: pr:<number> e.g. pr:357',
849
- );
850
- }
851
- atoms.push({ kind: 'pr', prNumber });
852
- break;
853
- }
866
+ case 'pr':
854
867
  case 'ci': {
855
- const prNumber = Number(payload);
856
- if (!Number.isInteger(prNumber) || prNumber <= 0) {
868
+ const ref = parsePrReference(payload);
869
+ if (!ref) {
857
870
  throw new EvidenceParseError(
858
- `ci atom requires a positive integer PR number, got "${payload}" in "${chunk}"`,
859
- 'Use format: ci:<number> e.g. ci:357',
871
+ `${kind} atom requires a positive integer PR number, got "${payload}" in "${chunk}"`,
872
+ `Use format: ${kind}:<number> e.g. ${kind}:357, or ${kind}:<component>@<integration> e.g. ${kind}:356@357`,
860
873
  );
861
874
  }
862
- atoms.push({ kind: 'ci', prNumber });
875
+ atoms.push(kind === 'pr' ? { kind: 'pr', ...ref } : { kind: 'ci', ...ref });
863
876
  break;
864
877
  }
865
878
  case 'loc-drop': {
package/src/index.ts CHANGED
@@ -516,6 +516,7 @@ export type {
516
516
  // === Done planner: read-only `cleo done --plan` evidence plan (T12623, T12624) ===
517
517
  export type {
518
518
  ChangeSetDoc,
519
+ ChangeSetMergeState,
519
520
  ChangeSetPrCandidate,
520
521
  ChangeSetRootSource,
521
522
  ChangeSetSource,
@@ -715,7 +715,9 @@ export type DocsLifecycleStatus = (typeof DOCS_LIFECYCLE_STATUSES)[number];
715
715
  * {@link DocsSupersedeParams}-style flows, NO supersession edge is created
716
716
  * (callers wanting an explicit lineage edge should use `cleo docs supersede`).
717
717
  *
718
- * Exactly one of `file` or `content` MUST be provided.
718
+ * At most one of `file` or `content` may be provided. With neither, `status`
719
+ * is required and the update changes only the lifecycle status, keeping the
720
+ * stored bytes (T12654).
719
721
  *
720
722
  * @task T10161 (Epic T10157 / Saga T9855 — E12.C4)
721
723
  */
@@ -738,7 +740,8 @@ export interface DocsUpdateParams {
738
740
  /**
739
741
  * Override the new lifecycle status. Defaults to `'draft'` on every update
740
742
  * so an explicit `accepted` doc gets back-pressured to draft on edit. Pass
741
- * `--status accepted` (or any other valid status) to override.
743
+ * `--status accepted` (or any other valid status) to override. Passed alone
744
+ * (no `file`/`content`), it changes only the lifecycle status.
742
745
  */
743
746
  status?: DocsLifecycleStatus;
744
747
  /** Validate/preflight only; do not mutate attachment rows, blob storage, or audit logs. */
package/src/task.ts CHANGED
@@ -170,6 +170,12 @@ export interface EvidenceValidationContext {
170
170
  criteria: ReadonlyArray<Pick<AcRow, 'id' | 'text' | 'updatedAt'>>;
171
171
  /** Verified PR merge whose immutable artifact bytes must be inspected. */
172
172
  artifactCommitSha?: string;
173
+ /**
174
+ * Validate without persisting anything: `pr:`/`ci:` lookups write neither
175
+ * the PR-result cache nor the branch-protection cache (T12671 review — the
176
+ * `cleo done --plan` preview must leave `.cleo/` untouched).
177
+ */
178
+ readOnly?: boolean;
173
179
  }
174
180
 
175
181
  /** Explicit criterion link to inspected artifacts and validated results in one gate receipt. */
@@ -371,10 +377,19 @@ export type EvidenceAtom =
371
377
  successCount: number;
372
378
  /** Total number of checks evaluated in the rollup. */
373
379
  totalChecks: number;
374
- /** Complete PR changed-file inventory retained for task-scope checks and display. */
380
+ /**
381
+ * Changed-file inventory retained for task-scope checks and display. With
382
+ * `componentPrNumber`, the component PR's files that survive in this
383
+ * PR's merge commit — never the whole integration PR.
384
+ */
375
385
  changedPaths?: string[];
376
386
  /** Explicit task relationship established when this atom was accepted. */
377
387
  taskId?: string;
388
+ /**
389
+ * Component PR this (integration) PR landed on the default branch
390
+ * (T12671): the task is linked through it and its files are the change.
391
+ */
392
+ componentPrNumber?: number;
378
393
  }
379
394
  | {
380
395
  /**
@@ -410,6 +425,8 @@ export type EvidenceAtom =
410
425
  testedTree?: string;
411
426
  /** Task the PR was verified to be linked to (T12634). */
412
427
  taskId?: string;
428
+ /** Component PR the task is linked through; its CI ran as this integration PR's (T12671). */
429
+ componentPrNumber?: number;
413
430
  /** Which configured checks attest each gate (`evidence.ciChecks`). */
414
431
  gateChecks?: { testsPassed?: string[]; qaPassed?: string[] };
415
432
  /** Where the required-check list came from (env, project context, branch protection). */