@pome-sh/cli 0.15.0 → 0.16.0
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/dist/build-info.json +3 -3
- package/dist/src/cli/checks.d.ts +3 -2
- package/dist/src/cli/checks.js +22 -9
- package/node_modules/@pome-sh/twin-linear/CHANGELOG.md +41 -0
- package/node_modules/@pome-sh/twin-linear/dist/src/check-comments.d.ts +10 -0
- package/node_modules/@pome-sh/twin-linear/dist/src/check-comments.js +117 -0
- package/node_modules/@pome-sh/twin-linear/dist/src/check-comments.js.map +1 -0
- package/node_modules/@pome-sh/twin-linear/dist/src/check-issues.d.ts +25 -0
- package/node_modules/@pome-sh/twin-linear/dist/src/check-issues.js +211 -0
- package/node_modules/@pome-sh/twin-linear/dist/src/check-issues.js.map +1 -0
- package/node_modules/@pome-sh/twin-linear/dist/src/check-kind.d.ts +3 -0
- package/node_modules/@pome-sh/twin-linear/dist/src/check-kind.js +9 -0
- package/node_modules/@pome-sh/twin-linear/dist/src/check-kind.js.map +1 -0
- package/node_modules/@pome-sh/twin-linear/dist/src/check-params.d.ts +8 -0
- package/node_modules/@pome-sh/twin-linear/dist/src/check-params.js +94 -0
- package/node_modules/@pome-sh/twin-linear/dist/src/check-params.js.map +1 -0
- package/node_modules/@pome-sh/twin-linear/dist/src/check-state.d.ts +98 -0
- package/node_modules/@pome-sh/twin-linear/dist/src/check-state.js +131 -0
- package/node_modules/@pome-sh/twin-linear/dist/src/check-state.js.map +1 -0
- package/node_modules/@pome-sh/twin-linear/dist/src/check-tape.d.ts +2 -0
- package/node_modules/@pome-sh/twin-linear/dist/src/check-tape.js +100 -0
- package/node_modules/@pome-sh/twin-linear/dist/src/check-tape.js.map +1 -0
- package/node_modules/@pome-sh/twin-linear/dist/src/check-worlds.d.ts +44 -0
- package/node_modules/@pome-sh/twin-linear/dist/src/check-worlds.js +93 -0
- package/node_modules/@pome-sh/twin-linear/dist/src/check-worlds.js.map +1 -0
- package/node_modules/@pome-sh/twin-linear/dist/src/checks.d.ts +29 -0
- package/node_modules/@pome-sh/twin-linear/dist/src/checks.js +52 -0
- package/node_modules/@pome-sh/twin-linear/dist/src/checks.js.map +1 -0
- package/node_modules/@pome-sh/twin-linear/dist/src/index.d.ts +2 -0
- package/node_modules/@pome-sh/twin-linear/dist/src/index.js +7 -0
- package/node_modules/@pome-sh/twin-linear/dist/src/index.js.map +1 -1
- package/node_modules/@pome-sh/twin-linear/package.json +1 -1
- package/package.json +2 -2
- package/tasks/24-linear-issue-triage.md +2 -2
- package/tasks/25-linear-comment-label-triage.md +3 -3
- package/tasks/26-github-linear-handoff.md +3 -4
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
//
|
|
3
|
+
// The typed slots Linear's declared checks fill (F-1129).
|
|
4
|
+
//
|
|
5
|
+
// They live in the twin, not the sdk, for the same reason the declarations do:
|
|
6
|
+
// the twin owns what a Linear team key or workflow state name may look like.
|
|
7
|
+
// Every pattern is capture-group-free — `defineCheck` throws otherwise, because
|
|
8
|
+
// each consumer reads capture group i+1 as slot i and one smuggled group hands
|
|
9
|
+
// every predicate its neighbour's argument.
|
|
10
|
+
//
|
|
11
|
+
// NO SLOT MAY SIT AT THE END OF A TEMPLATE UNBOUNDED. The contract suite
|
|
12
|
+
// requires that `"${rendered} and also something else"` does not match, and an
|
|
13
|
+
// anchored `[^"\n]+` in terminal position would swallow the suffix. Every
|
|
14
|
+
// free-text slot below is delimited by a quote or a backtick in the templates
|
|
15
|
+
// that use it — which is also why Linear's state slot is quoted where
|
|
16
|
+
// `github.issue-state`'s is bare.
|
|
17
|
+
// Double-quote delimited. Linear puts no constraint on an issue title beyond
|
|
18
|
+
// non-empty, so the quote is the only exclusion.
|
|
19
|
+
export const issueTitle = {
|
|
20
|
+
name: "title",
|
|
21
|
+
pattern: '[^"\\n]+',
|
|
22
|
+
example: "Orders 500 after deploy",
|
|
23
|
+
render: (value) => value,
|
|
24
|
+
parse: (raw) => raw,
|
|
25
|
+
};
|
|
26
|
+
// Linear's own team-key constraint, from `seedSchema` (`seed.ts`):
|
|
27
|
+
// /^[A-Z][A-Z0-9]*$/.
|
|
28
|
+
//
|
|
29
|
+
// This is the SCOPE slot twin-github's repo rule requires of every state check,
|
|
30
|
+
// and Linear inherits that rule rather than arguing it away as twin-slack did:
|
|
31
|
+
// `seed.ts:319-325` validates issue-title uniqueness PER TEAM, so a title-keyed
|
|
32
|
+
// selector over a two-team world is exactly the ambiguity the rule closes.
|
|
33
|
+
//
|
|
34
|
+
// Its shape is also why a reason string may quote it — an uppercase
|
|
35
|
+
// alphanumeric run matches no pattern in either redactor.
|
|
36
|
+
export const teamKey = {
|
|
37
|
+
name: "team",
|
|
38
|
+
pattern: "[A-Z][A-Z0-9]*",
|
|
39
|
+
example: "ENG",
|
|
40
|
+
render: (value) => value,
|
|
41
|
+
parse: (raw) => raw,
|
|
42
|
+
};
|
|
43
|
+
// NOT a closed set, where `github.issue-state`'s is. GitHub has exactly two
|
|
44
|
+
// issue states; Linear workflow state names are user-defined per team
|
|
45
|
+
// (`seed.ts` `teams[].states[].name` is free text), so a closed alternation
|
|
46
|
+
// would silently exclude every custom workflow. The template quotes it, which
|
|
47
|
+
// is what keeps a free-text slot safe in terminal position.
|
|
48
|
+
export const workflowStateName = {
|
|
49
|
+
name: "state",
|
|
50
|
+
pattern: '[^"\\n]+',
|
|
51
|
+
example: "In Progress",
|
|
52
|
+
render: (value) => value,
|
|
53
|
+
parse: (raw) => raw,
|
|
54
|
+
};
|
|
55
|
+
export const labelName = {
|
|
56
|
+
name: "label",
|
|
57
|
+
pattern: '[^"\\n]+',
|
|
58
|
+
example: "Agent",
|
|
59
|
+
render: (value) => value,
|
|
60
|
+
parse: (raw) => raw,
|
|
61
|
+
};
|
|
62
|
+
// A bare integer, no leading zeros, so `render(parse(x))` round-trips.
|
|
63
|
+
// `VACUITY_SENTINEL_NUMBER` (987654321) satisfies this pattern, which is what
|
|
64
|
+
// lets this slot carry a real mutant rather than an admitted null.
|
|
65
|
+
export const estimatePoints = {
|
|
66
|
+
name: "estimate",
|
|
67
|
+
pattern: "0|[1-9][0-9]*",
|
|
68
|
+
example: "2",
|
|
69
|
+
render: (value) => value,
|
|
70
|
+
parse: (raw) => raw,
|
|
71
|
+
};
|
|
72
|
+
// Backtick-delimited, matching `github.issue-assignee`'s `{login}`. The legacy
|
|
73
|
+
// rule matched a user by email, name OR displayName and this keeps all three —
|
|
74
|
+
// which is exactly why its check declares this as its `subject`: the team's
|
|
75
|
+
// `PII_PATTERNS` eats an email that the twin's own `SCRUB_STEPS` leaves alone,
|
|
76
|
+
// so without the declaration the criterion could never fire.
|
|
77
|
+
export const userRef = {
|
|
78
|
+
name: "user",
|
|
79
|
+
pattern: "[^`\\n]+",
|
|
80
|
+
example: "dev@pome-twin.test",
|
|
81
|
+
render: (value) => value,
|
|
82
|
+
parse: (raw) => raw,
|
|
83
|
+
};
|
|
84
|
+
// The value SCANNED inside free prose rather than compared to a field, which is
|
|
85
|
+
// why its check declares it as `subject` — twin-slack's `messageNeedle`
|
|
86
|
+
// precedent.
|
|
87
|
+
export const commentNeedle = {
|
|
88
|
+
name: "needle",
|
|
89
|
+
pattern: '[^"\\n]+',
|
|
90
|
+
example: "triage",
|
|
91
|
+
render: (value) => value,
|
|
92
|
+
parse: (raw) => raw,
|
|
93
|
+
};
|
|
94
|
+
//# sourceMappingURL=check-params.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check-params.js","sourceRoot":"","sources":["../../src/check-params.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,EAAE;AACF,0DAA0D;AAC1D,EAAE;AACF,+EAA+E;AAC/E,6EAA6E;AAC7E,gFAAgF;AAChF,+EAA+E;AAC/E,4CAA4C;AAC5C,EAAE;AACF,yEAAyE;AACzE,+EAA+E;AAC/E,0EAA0E;AAC1E,8EAA8E;AAC9E,sEAAsE;AACtE,kCAAkC;AAIlC,6EAA6E;AAC7E,iDAAiD;AACjD,MAAM,CAAC,MAAM,UAAU,GAAmB;IACxC,IAAI,EAAE,OAAO;IACb,OAAO,EAAE,UAAU;IACnB,OAAO,EAAE,yBAAyB;IAClC,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK;IACxB,KAAK,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG;CACpB,CAAC;AAEF,mEAAmE;AACnE,sBAAsB;AACtB,EAAE;AACF,gFAAgF;AAChF,+EAA+E;AAC/E,gFAAgF;AAChF,2EAA2E;AAC3E,EAAE;AACF,oEAAoE;AACpE,0DAA0D;AAC1D,MAAM,CAAC,MAAM,OAAO,GAAmB;IACrC,IAAI,EAAE,MAAM;IACZ,OAAO,EAAE,gBAAgB;IACzB,OAAO,EAAE,KAAK;IACd,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK;IACxB,KAAK,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG;CACpB,CAAC;AAEF,4EAA4E;AAC5E,sEAAsE;AACtE,4EAA4E;AAC5E,8EAA8E;AAC9E,4DAA4D;AAC5D,MAAM,CAAC,MAAM,iBAAiB,GAAmB;IAC/C,IAAI,EAAE,OAAO;IACb,OAAO,EAAE,UAAU;IACnB,OAAO,EAAE,aAAa;IACtB,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK;IACxB,KAAK,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG;CACpB,CAAC;AAEF,MAAM,CAAC,MAAM,SAAS,GAAmB;IACvC,IAAI,EAAE,OAAO;IACb,OAAO,EAAE,UAAU;IACnB,OAAO,EAAE,OAAO;IAChB,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK;IACxB,KAAK,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG;CACpB,CAAC;AAEF,uEAAuE;AACvE,8EAA8E;AAC9E,mEAAmE;AACnE,MAAM,CAAC,MAAM,cAAc,GAAmB;IAC5C,IAAI,EAAE,UAAU;IAChB,OAAO,EAAE,eAAe;IACxB,OAAO,EAAE,GAAG;IACZ,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK;IACxB,KAAK,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG;CACpB,CAAC;AAEF,+EAA+E;AAC/E,+EAA+E;AAC/E,4EAA4E;AAC5E,+EAA+E;AAC/E,6DAA6D;AAC7D,MAAM,CAAC,MAAM,OAAO,GAAmB;IACrC,IAAI,EAAE,MAAM;IACZ,OAAO,EAAE,UAAU;IACnB,OAAO,EAAE,oBAAoB;IAC7B,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK;IACxB,KAAK,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG;CACpB,CAAC;AAEF,gFAAgF;AAChF,wEAAwE;AACxE,aAAa;AACb,MAAM,CAAC,MAAM,aAAa,GAAmB;IAC3C,IAAI,EAAE,QAAQ;IACd,OAAO,EAAE,UAAU;IACnB,OAAO,EAAE,QAAQ;IACjB,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK;IACxB,KAAK,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG;CACpB,CAAC"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { CheckOutcome } from "@pome-sh/sdk/checks";
|
|
2
|
+
export interface LinearCheckStateTeam {
|
|
3
|
+
id?: string;
|
|
4
|
+
key?: string;
|
|
5
|
+
name?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface LinearCheckStateWorkflowState {
|
|
8
|
+
id?: string;
|
|
9
|
+
teamId?: string;
|
|
10
|
+
name?: string;
|
|
11
|
+
type?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface LinearCheckStateLabel {
|
|
14
|
+
id?: string;
|
|
15
|
+
teamId?: string;
|
|
16
|
+
name?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface LinearCheckStateIssue {
|
|
19
|
+
id?: string;
|
|
20
|
+
identifier?: string;
|
|
21
|
+
number?: number;
|
|
22
|
+
teamId?: string;
|
|
23
|
+
title?: string;
|
|
24
|
+
estimate?: number | null;
|
|
25
|
+
stateId?: string;
|
|
26
|
+
assigneeId?: string;
|
|
27
|
+
archivedAt?: string | null;
|
|
28
|
+
labelIds?: string[];
|
|
29
|
+
}
|
|
30
|
+
export interface LinearCheckStateComment {
|
|
31
|
+
id?: string;
|
|
32
|
+
issueId?: string;
|
|
33
|
+
parentId?: string | null;
|
|
34
|
+
userId?: string;
|
|
35
|
+
body?: string;
|
|
36
|
+
}
|
|
37
|
+
export interface LinearCheckStateUser {
|
|
38
|
+
id?: string;
|
|
39
|
+
email?: string;
|
|
40
|
+
name?: string;
|
|
41
|
+
displayName?: string;
|
|
42
|
+
}
|
|
43
|
+
export interface LinearCheckState {
|
|
44
|
+
teams?: LinearCheckStateTeam[] | null;
|
|
45
|
+
workflowStates?: LinearCheckStateWorkflowState[] | null;
|
|
46
|
+
labels?: LinearCheckStateLabel[] | null;
|
|
47
|
+
issues?: LinearCheckStateIssue[] | null;
|
|
48
|
+
comments?: LinearCheckStateComment[] | null;
|
|
49
|
+
users?: LinearCheckStateUser[] | null;
|
|
50
|
+
exportBounds?: {
|
|
51
|
+
truncatedCollections?: string[];
|
|
52
|
+
} | null;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* twin-github's `Resolved<T>` with one field added.
|
|
56
|
+
*
|
|
57
|
+
* github's resolvers always FAIL on a miss and twin-slack's always SKIP. Linear
|
|
58
|
+
* needs both from the same resolver, because a miss inside a truncated
|
|
59
|
+
* collection is evidence of a partial export while a miss in a complete one is
|
|
60
|
+
* evidence about the examinee. Carrying the disposition on the value is what
|
|
61
|
+
* stops every call site from re-deciding it — and re-deciding it wrongly is how
|
|
62
|
+
* a do-nothing agent scores 100% on task 26.
|
|
63
|
+
*/
|
|
64
|
+
export type Resolved<T> = {
|
|
65
|
+
found: T;
|
|
66
|
+
} | {
|
|
67
|
+
missing: string;
|
|
68
|
+
skip: boolean;
|
|
69
|
+
};
|
|
70
|
+
/** The one place an unresolved selector becomes an outcome. */
|
|
71
|
+
export declare function unresolved(r: {
|
|
72
|
+
missing: string;
|
|
73
|
+
skip: boolean;
|
|
74
|
+
}): CheckOutcome;
|
|
75
|
+
export declare function isTruncated(state: LinearCheckState, collection: string): boolean;
|
|
76
|
+
/**
|
|
77
|
+
* The issue an assertion is about, by exact title, scoped to one team.
|
|
78
|
+
*
|
|
79
|
+
* Exact rather than case-insensitive: the task names the title it seeded or
|
|
80
|
+
* asked for, and a case-folding match would let an examinee half-comply.
|
|
81
|
+
*/
|
|
82
|
+
export declare function resolveIssue(state: LinearCheckState, teamKey: string, title: string): Resolved<LinearCheckStateIssue>;
|
|
83
|
+
/** Trap 1: `stateId` → the team's own workflow-state row. */
|
|
84
|
+
export declare function resolveWorkflowStateName(state: LinearCheckState, issue: LinearCheckStateIssue): Resolved<string>;
|
|
85
|
+
/**
|
|
86
|
+
* Trap 2: `labelIds` → catalog names, lowercased.
|
|
87
|
+
*
|
|
88
|
+
* Case-insensitive because the legacy rule's `eqi` was, and because a label an
|
|
89
|
+
* examinee creates is prose it retyped.
|
|
90
|
+
*/
|
|
91
|
+
export declare function resolveLabelNames(state: LinearCheckState, issue: LinearCheckStateIssue): Resolved<Set<string>>;
|
|
92
|
+
export declare function resolveComments(state: LinearCheckState, issue: LinearCheckStateIssue): Resolved<LinearCheckStateComment[]>;
|
|
93
|
+
/**
|
|
94
|
+
* Every spelling of a user the legacy `linear.issue-assignee` rule accepted —
|
|
95
|
+
* email, name, displayName — in that order, so a reason string names the most
|
|
96
|
+
* specific one first.
|
|
97
|
+
*/
|
|
98
|
+
export declare function resolveUserLabels(state: LinearCheckState, id: string | undefined): string[];
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
//
|
|
3
|
+
// How a declared check READS the exported Linear workspace (F-1129).
|
|
4
|
+
//
|
|
5
|
+
// `LinearStateExport` (`state.ts:4-27`) names the collections but types every
|
|
6
|
+
// row `unknown[]`, so the row shapes still have to be modelled by hand here, as
|
|
7
|
+
// twin-slack's `check-state.ts` does. Every field is optional and every list
|
|
8
|
+
// nullable: an older snapshot, a partial upload, or a schema that gained a
|
|
9
|
+
// column all arrive as absent fields. A predicate that assumes presence throws
|
|
10
|
+
// inside the evaluator; one that reads this model returns a NAMED verdict. That
|
|
11
|
+
// is the D4 bargain — a criterion may leave the denominator, but it may never
|
|
12
|
+
// fabricate one.
|
|
13
|
+
//
|
|
14
|
+
// Five shapes are counter-intuitive and each has a wrong-verdict story:
|
|
15
|
+
//
|
|
16
|
+
// 1. AN ISSUE HAS NO STATE STRING. `issue.stateId` references a
|
|
17
|
+
// `workflowStates` row, and workflow states are scoped by `teamId` — so a
|
|
18
|
+
// join that is not team-scoped resolves the wrong row in a two-team world.
|
|
19
|
+
// 2. LABELS ARE A THIRD SHAPE. The SEED writes label names; twin-github's
|
|
20
|
+
// export writes label objects; this export writes `issue.labelIds:
|
|
21
|
+
// string[]`, which must be joined to `labels[].name`. One concept, three
|
|
22
|
+
// shapes, and only this one is exported.
|
|
23
|
+
// 3. `archivedAt` IS EXPORTED. An archived issue is still a row in `issues`.
|
|
24
|
+
// Resolution treats archived as absent, so an existence assertion cannot
|
|
25
|
+
// be satisfied by an issue the examinee archived.
|
|
26
|
+
// 4. `exportBounds.truncatedCollections` NAMES WHAT WAS CAPPED.
|
|
27
|
+
// `STATE_EXPORT_CAP = 2000` (`types.ts:394`) drops rows past the cap, and
|
|
28
|
+
// this is the ONLY place in the vocabulary where "not found" and "absent"
|
|
29
|
+
// are different facts. Neither GitHubCheckState nor SlackCheckState
|
|
30
|
+
// carries an analogue, which is why this contract is Linear's alone.
|
|
31
|
+
// 5. TITLE UNIQUENESS IS SEED-TIME ONLY. `seed.ts:319-325` validates
|
|
32
|
+
// `issueTitlesByTeam` at parse; nothing enforces it at runtime, so an
|
|
33
|
+
// examinee can create a duplicate. Two matches FAIL naming the count —
|
|
34
|
+
// silently grading the first is the wrong-match hazard twin-github's repo
|
|
35
|
+
// rule exists to close, one level down.
|
|
36
|
+
//
|
|
37
|
+
// REASON STRINGS NEVER ECHO A TITLE. Only a check's declared `subject` is
|
|
38
|
+
// guaranteed to have survived the redaction pipeline by the time `evaluate`
|
|
39
|
+
// runs (`evaluate.ts:141-144`), and `title` is a selector on every check except
|
|
40
|
+
// `linear.issue-exists`. The team key is safe to quote: `[A-Z][A-Z0-9]*`
|
|
41
|
+
// matches no pattern in either redactor.
|
|
42
|
+
/** The one place an unresolved selector becomes an outcome. */
|
|
43
|
+
export function unresolved(r) {
|
|
44
|
+
return r.skip
|
|
45
|
+
? { passed: false, status: "skipped", reason: r.missing }
|
|
46
|
+
: { passed: false, reason: r.missing };
|
|
47
|
+
}
|
|
48
|
+
export function isTruncated(state, collection) {
|
|
49
|
+
return (state.exportBounds?.truncatedCollections ?? []).includes(collection);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The issue an assertion is about, by exact title, scoped to one team.
|
|
53
|
+
*
|
|
54
|
+
* Exact rather than case-insensitive: the task names the title it seeded or
|
|
55
|
+
* asked for, and a case-folding match would let an examinee half-comply.
|
|
56
|
+
*/
|
|
57
|
+
export function resolveIssue(state, teamKey, title) {
|
|
58
|
+
if (state.teams == null)
|
|
59
|
+
return { missing: "state_incomplete", skip: true };
|
|
60
|
+
const team = state.teams.find((t) => t.key === teamKey);
|
|
61
|
+
if (team?.id == null) {
|
|
62
|
+
return { missing: `team \`${teamKey}\` not found in state_final`, skip: false };
|
|
63
|
+
}
|
|
64
|
+
if (state.issues == null)
|
|
65
|
+
return { missing: "state_incomplete", skip: true };
|
|
66
|
+
const matches = state.issues.filter((issue) => issue.teamId === team.id &&
|
|
67
|
+
issue.title === title &&
|
|
68
|
+
(issue.archivedAt ?? null) === null);
|
|
69
|
+
if (matches.length === 1)
|
|
70
|
+
return { found: matches[0] };
|
|
71
|
+
if (matches.length > 1) {
|
|
72
|
+
return {
|
|
73
|
+
missing: `${matches.length} issues in \`${teamKey}\` share that title`,
|
|
74
|
+
skip: false,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
// The only skip a miss can earn, and it is earned by evidence rather than by
|
|
78
|
+
// taste: the twin itself reported that `issues` lost rows to the export cap.
|
|
79
|
+
if (isTruncated(state, "issues"))
|
|
80
|
+
return { missing: "state_truncated", skip: true };
|
|
81
|
+
return { missing: `no issue with that title in \`${teamKey}\``, skip: false };
|
|
82
|
+
}
|
|
83
|
+
/** Trap 1: `stateId` → the team's own workflow-state row. */
|
|
84
|
+
export function resolveWorkflowStateName(state, issue) {
|
|
85
|
+
if (state.workflowStates == null)
|
|
86
|
+
return { missing: "state_incomplete", skip: true };
|
|
87
|
+
const row = state.workflowStates.find((s) => s.id === issue.stateId && s.teamId === issue.teamId);
|
|
88
|
+
if (row?.name == null)
|
|
89
|
+
return { missing: "workflow_state_unresolved", skip: true };
|
|
90
|
+
return { found: row.name };
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Trap 2: `labelIds` → catalog names, lowercased.
|
|
94
|
+
*
|
|
95
|
+
* Case-insensitive because the legacy rule's `eqi` was, and because a label an
|
|
96
|
+
* examinee creates is prose it retyped.
|
|
97
|
+
*/
|
|
98
|
+
export function resolveLabelNames(state, issue) {
|
|
99
|
+
if (state.labels == null)
|
|
100
|
+
return { missing: "state_incomplete", skip: true };
|
|
101
|
+
const byId = new Map(state.labels.filter((l) => l.id != null).map((l) => [l.id, (l.name ?? "").toLowerCase()]));
|
|
102
|
+
const names = new Set();
|
|
103
|
+
for (const id of issue.labelIds ?? []) {
|
|
104
|
+
const name = byId.get(id);
|
|
105
|
+
// A link to a label the export did not carry is a partial export, not a
|
|
106
|
+
// verdict about the examinee.
|
|
107
|
+
if (name === undefined)
|
|
108
|
+
return { missing: "label_unresolved", skip: true };
|
|
109
|
+
names.add(name);
|
|
110
|
+
}
|
|
111
|
+
return { found: names };
|
|
112
|
+
}
|
|
113
|
+
export function resolveComments(state, issue) {
|
|
114
|
+
if (state.comments == null)
|
|
115
|
+
return { missing: "state_incomplete", skip: true };
|
|
116
|
+
return { found: state.comments.filter((c) => c.issueId === issue.id) };
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Every spelling of a user the legacy `linear.issue-assignee` rule accepted —
|
|
120
|
+
* email, name, displayName — in that order, so a reason string names the most
|
|
121
|
+
* specific one first.
|
|
122
|
+
*/
|
|
123
|
+
export function resolveUserLabels(state, id) {
|
|
124
|
+
if (id == null || state.users == null)
|
|
125
|
+
return [];
|
|
126
|
+
const user = state.users.find((u) => u.id === id);
|
|
127
|
+
if (user == null)
|
|
128
|
+
return [];
|
|
129
|
+
return [user.email, user.name, user.displayName].filter((v) => typeof v === "string" && v.length > 0);
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=check-state.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check-state.js","sourceRoot":"","sources":["../../src/check-state.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
//
|
|
3
|
+
// What Linear's declared checks can assert about the RUN — the recorded call
|
|
4
|
+
// tape rather than the exported end state (F-1129).
|
|
5
|
+
//
|
|
6
|
+
// Why this class has to exist at all: an unsupported call leaves NO STATE
|
|
7
|
+
// TRACE. The twin answers 501 and mutates nothing, so `state_final.json` is
|
|
8
|
+
// byte-identical whether the examinee reached for an unimplemented route or
|
|
9
|
+
// never tried. The runtime stamps `fidelity: "unsupported"` on the recorded
|
|
10
|
+
// event instead, and that stamp is the only place the fact survives.
|
|
11
|
+
//
|
|
12
|
+
// NO TWIN WORD IN THE TEMPLATE, matching `github.no-unsupported-endpoint`
|
|
13
|
+
// exactly. The legacy cloud-side rule took the twin word as an OPTIONAL
|
|
14
|
+
// qualifier ("No unsupported Linear endpoint was called") along with plural and
|
|
15
|
+
// `were` variants, because an author typed English. Under position 2 an author
|
|
16
|
+
// PICKS the check, so the variants are retired rather than ported. Two twins
|
|
17
|
+
// may share a template — ids differ, and `resolveTwinRules` is per-twin — and
|
|
18
|
+
// no shipped Linear criterion says this sentence today, so nothing in the
|
|
19
|
+
// corpus moves.
|
|
20
|
+
import { defineCheck } from "@pome-sh/sdk/checks";
|
|
21
|
+
import { tapeWorld } from "./check-worlds.js";
|
|
22
|
+
export const noUnsupportedEndpoint = defineCheck({
|
|
23
|
+
id: "linear.no-unsupported-endpoint",
|
|
24
|
+
description: "Scans the recorded call tape for any request the twin answered with fidelity " +
|
|
25
|
+
'"unsupported" — a route it does not implement, answered 501. It asserts nothing about ' +
|
|
26
|
+
"whether the run SUCCEEDED, and nothing about calls that were merely rejected: a 404 or a " +
|
|
27
|
+
"422 from a route the twin does implement is a semantic answer and passes. The tape is " +
|
|
28
|
+
"scoped to this twin by the engine before the check sees it, so an unsupported call to a " +
|
|
29
|
+
"different twin in a multi-twin session cannot fail it.",
|
|
30
|
+
template: "No unsupported endpoint was called",
|
|
31
|
+
params: {},
|
|
32
|
+
substrate: "tape",
|
|
33
|
+
// A prohibition. Nothing is required to happen; only the examinee reaching
|
|
34
|
+
// for an unimplemented route can break it.
|
|
35
|
+
polarity: () => "negative",
|
|
36
|
+
// No caller-supplied literal is hunted for in any substrate, so there is
|
|
37
|
+
// nothing a redactor could silently delete out from under this check.
|
|
38
|
+
subject: () => null,
|
|
39
|
+
// Ledgered. No slots, so the sentence carries no literal to falsify — the
|
|
40
|
+
// trigger is a fidelity stamp on the tape, which no mutation of the criterion
|
|
41
|
+
// text can reach.
|
|
42
|
+
vacuityMutant: () => null,
|
|
43
|
+
discriminatingWorlds: () => ({
|
|
44
|
+
passing: tapeWorld([
|
|
45
|
+
{
|
|
46
|
+
twin: "linear",
|
|
47
|
+
method: "POST",
|
|
48
|
+
path: "/graphql",
|
|
49
|
+
status: 200,
|
|
50
|
+
fidelity: "semantic",
|
|
51
|
+
event_id: "evt_ok",
|
|
52
|
+
},
|
|
53
|
+
]),
|
|
54
|
+
failing: tapeWorld([
|
|
55
|
+
{
|
|
56
|
+
twin: "linear",
|
|
57
|
+
method: "POST",
|
|
58
|
+
path: "/graphql",
|
|
59
|
+
status: 501,
|
|
60
|
+
fidelity: "unsupported",
|
|
61
|
+
event_id: "evt_bad",
|
|
62
|
+
},
|
|
63
|
+
]),
|
|
64
|
+
}),
|
|
65
|
+
evaluate(_args, { tape }) {
|
|
66
|
+
// The engine guards this before calling; the check guards too, so a
|
|
67
|
+
// consumer that forgets gets a named skip rather than a vacuous pass. For a
|
|
68
|
+
// NEGATIVE criterion that distinction is the whole ballgame — passing here
|
|
69
|
+
// would be a clean bill of health issued over a tape nobody read.
|
|
70
|
+
//
|
|
71
|
+
// `null` and `[]` are deliberately different: an EMPTY tape is a real world
|
|
72
|
+
// (the agent called nothing, so it called nothing unsupported).
|
|
73
|
+
if (tape === null)
|
|
74
|
+
return { passed: false, reason: "tape_missing", status: "skipped" };
|
|
75
|
+
const unsupported = tape.filter((event) => event.fidelity === "unsupported");
|
|
76
|
+
if (unsupported.length === 0) {
|
|
77
|
+
// No evidence on the pass branch (F-980). This asserts a NEGATIVE over
|
|
78
|
+
// the whole tape, and a negative over an empty set has no single call to
|
|
79
|
+
// point at; citing all N inspected calls would be a copy of the trace.
|
|
80
|
+
return {
|
|
81
|
+
passed: true,
|
|
82
|
+
reason: `no unsupported endpoint was called (${tape.length} call(s) inspected)`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
// Cite the offenders. A row with no `event_id` drops out of the CITATION
|
|
86
|
+
// but not out of the count or the prose: losing an id must never lose a
|
|
87
|
+
// finding.
|
|
88
|
+
const evidenceEventIds = unsupported
|
|
89
|
+
.map((event) => event.event_id)
|
|
90
|
+
.filter((id) => typeof id === "string" && id !== "");
|
|
91
|
+
return {
|
|
92
|
+
passed: false,
|
|
93
|
+
reason: `${unsupported.length} unsupported call(s): ${unsupported
|
|
94
|
+
.map((e) => `${e.method ?? "?"} ${e.path ?? "?"}`)
|
|
95
|
+
.join(", ")}`,
|
|
96
|
+
...(evidenceEventIds.length > 0 ? { evidenceEventIds } : {}),
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
//# sourceMappingURL=check-tape.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check-tape.js","sourceRoot":"","sources":["../../src/check-tape.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { CheckSubstrate, CheckTapeEvent } from "@pome-sh/sdk/checks";
|
|
2
|
+
import type { LinearCheckState, LinearCheckStateComment, LinearCheckStateIssue } from "./check-state.js";
|
|
3
|
+
export declare const FIXTURE_TEAM_ID = "team_eng";
|
|
4
|
+
export declare const FIXTURE_TEAM_KEY = "ENG";
|
|
5
|
+
/** The one derivation, used by both the catalog and the rows that reference it. */
|
|
6
|
+
export declare function fixtureStateId(name: string): string;
|
|
7
|
+
/** Ditto for labels. */
|
|
8
|
+
export declare function fixtureLabelId(name: string): string;
|
|
9
|
+
/** Linear's default workflow, the five states `defaultSeedState()` seeds. */
|
|
10
|
+
export declare const FIXTURE_STATES: readonly [{
|
|
11
|
+
readonly name: "Backlog";
|
|
12
|
+
readonly type: "backlog";
|
|
13
|
+
}, {
|
|
14
|
+
readonly name: "Todo";
|
|
15
|
+
readonly type: "unstarted";
|
|
16
|
+
}, {
|
|
17
|
+
readonly name: "In Progress";
|
|
18
|
+
readonly type: "started";
|
|
19
|
+
}, {
|
|
20
|
+
readonly name: "Done";
|
|
21
|
+
readonly type: "completed";
|
|
22
|
+
}, {
|
|
23
|
+
readonly name: "Canceled";
|
|
24
|
+
readonly type: "canceled";
|
|
25
|
+
}];
|
|
26
|
+
export declare function finalWorld(final: LinearCheckState): CheckSubstrate<LinearCheckState>;
|
|
27
|
+
export declare function deltaWorld(seed: LinearCheckState, final: LinearCheckState): CheckSubstrate<LinearCheckState>;
|
|
28
|
+
export declare function tapeWorld(tape: readonly CheckTapeEvent[]): CheckSubstrate<LinearCheckState>;
|
|
29
|
+
/**
|
|
30
|
+
* A one-team workspace with Linear's five default workflow states.
|
|
31
|
+
*
|
|
32
|
+
* `labelNames` is the workspace label CATALOG, not the labels on any issue —
|
|
33
|
+
* `issueRow` attaches ids, and an id with no catalog row resolves to
|
|
34
|
+
* `label_unresolved`, a skip. Pass every label the issues reference.
|
|
35
|
+
*/
|
|
36
|
+
export declare function linearState(issues: LinearCheckStateIssue[], comments?: LinearCheckStateComment[], labelNames?: readonly string[]): LinearCheckState;
|
|
37
|
+
/** A resolvable issue row, with `stateId` and `labelIds` derived the way the export emits them. */
|
|
38
|
+
export declare function issueRow(title: string, over?: {
|
|
39
|
+
id?: string;
|
|
40
|
+
stateName?: string;
|
|
41
|
+
labelNames?: readonly string[];
|
|
42
|
+
estimate?: number | null;
|
|
43
|
+
assigneeId?: string;
|
|
44
|
+
}): LinearCheckStateIssue;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
//
|
|
3
|
+
// The fixture worlds Linear's declarations name (F-1129).
|
|
4
|
+
//
|
|
5
|
+
// In `src/` rather than `test/` for the same reason twin-github's and
|
|
6
|
+
// twin-slack's are: `discriminatingWorlds` is a DECLARED field read from npm by
|
|
7
|
+
// pome-cloud and the CLI, so a builder that shipped only in the test tree would
|
|
8
|
+
// make the field unusable outside this repo.
|
|
9
|
+
//
|
|
10
|
+
// EVERY FAILING WORLD KEEPS ITS SELECTOR RESOLVABLE. Arm 3 of
|
|
11
|
+
// `probeDiscrimination` rejects a failing world whose `reason` matches the one
|
|
12
|
+
// an EMPTY world produces — measured on 11 of twin-github's 13 checks before
|
|
13
|
+
// that arm existed. So a world built by omitting the team or the issue fails
|
|
14
|
+
// through its selector rather than its assertion and is thrown out. Which is
|
|
15
|
+
// why `linearState` always fills teams, workflowStates and labels, and why the
|
|
16
|
+
// declarations move only the asserted value between their two worlds.
|
|
17
|
+
//
|
|
18
|
+
// The id derivations are shared rather than written twice. A fixture whose
|
|
19
|
+
// `stateId` did not match a row in the same world would skip
|
|
20
|
+
// `workflow_state_unresolved`, and a skip satisfies NEITHER arm — so a drifting
|
|
21
|
+
// literal would look like a broken check rather than a broken fixture.
|
|
22
|
+
export const FIXTURE_TEAM_ID = "team_eng";
|
|
23
|
+
export const FIXTURE_TEAM_KEY = "ENG";
|
|
24
|
+
/** The one derivation, used by both the catalog and the rows that reference it. */
|
|
25
|
+
export function fixtureStateId(name) {
|
|
26
|
+
return `state_${name.toLowerCase().replace(/[^a-z0-9]+/g, "_")}`;
|
|
27
|
+
}
|
|
28
|
+
/** Ditto for labels. */
|
|
29
|
+
export function fixtureLabelId(name) {
|
|
30
|
+
return `label_${name.toLowerCase().replace(/[^a-z0-9]+/g, "_")}`;
|
|
31
|
+
}
|
|
32
|
+
/** Linear's default workflow, the five states `defaultSeedState()` seeds. */
|
|
33
|
+
export const FIXTURE_STATES = [
|
|
34
|
+
{ name: "Backlog", type: "backlog" },
|
|
35
|
+
{ name: "Todo", type: "unstarted" },
|
|
36
|
+
{ name: "In Progress", type: "started" },
|
|
37
|
+
{ name: "Done", type: "completed" },
|
|
38
|
+
{ name: "Canceled", type: "canceled" },
|
|
39
|
+
];
|
|
40
|
+
export function finalWorld(final) {
|
|
41
|
+
return { seed: null, final, tape: null };
|
|
42
|
+
}
|
|
43
|
+
export function deltaWorld(seed, final) {
|
|
44
|
+
return { seed, final, tape: null };
|
|
45
|
+
}
|
|
46
|
+
export function tapeWorld(tape) {
|
|
47
|
+
return { seed: null, final: { issues: [] }, tape };
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* A one-team workspace with Linear's five default workflow states.
|
|
51
|
+
*
|
|
52
|
+
* `labelNames` is the workspace label CATALOG, not the labels on any issue —
|
|
53
|
+
* `issueRow` attaches ids, and an id with no catalog row resolves to
|
|
54
|
+
* `label_unresolved`, a skip. Pass every label the issues reference.
|
|
55
|
+
*/
|
|
56
|
+
export function linearState(issues, comments = [], labelNames = ["Agent"]) {
|
|
57
|
+
return {
|
|
58
|
+
teams: [{ id: FIXTURE_TEAM_ID, key: FIXTURE_TEAM_KEY, name: "Engineering" }],
|
|
59
|
+
workflowStates: FIXTURE_STATES.map((s) => ({
|
|
60
|
+
id: fixtureStateId(s.name),
|
|
61
|
+
teamId: FIXTURE_TEAM_ID,
|
|
62
|
+
name: s.name,
|
|
63
|
+
type: s.type,
|
|
64
|
+
})),
|
|
65
|
+
labels: labelNames.map((name) => ({
|
|
66
|
+
id: fixtureLabelId(name),
|
|
67
|
+
teamId: FIXTURE_TEAM_ID,
|
|
68
|
+
name,
|
|
69
|
+
})),
|
|
70
|
+
issues,
|
|
71
|
+
comments,
|
|
72
|
+
users: [
|
|
73
|
+
{ id: "user_dev", email: "dev@pome-twin.test", name: "Developer", displayName: "Dev" },
|
|
74
|
+
],
|
|
75
|
+
exportBounds: { truncatedCollections: [] },
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** A resolvable issue row, with `stateId` and `labelIds` derived the way the export emits them. */
|
|
79
|
+
export function issueRow(title, over = {}) {
|
|
80
|
+
return {
|
|
81
|
+
id: over.id ?? "issue_1",
|
|
82
|
+
identifier: "ENG-1",
|
|
83
|
+
number: 1,
|
|
84
|
+
teamId: FIXTURE_TEAM_ID,
|
|
85
|
+
title,
|
|
86
|
+
stateId: fixtureStateId(over.stateName ?? "In Progress"),
|
|
87
|
+
estimate: over.estimate ?? null,
|
|
88
|
+
...(over.assigneeId === undefined ? {} : { assigneeId: over.assigneeId }),
|
|
89
|
+
archivedAt: null,
|
|
90
|
+
labelIds: (over.labelNames ?? []).map(fixtureLabelId),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=check-worlds.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check-worlds.js","sourceRoot":"","sources":["../../src/check-worlds.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type { Check } from "./check-kind.js";
|
|
2
|
+
export type { LinearCheckState, LinearCheckStateComment, LinearCheckStateIssue, LinearCheckStateLabel, LinearCheckStateTeam, LinearCheckStateUser, LinearCheckStateWorkflowState, } from "./check-state.js";
|
|
3
|
+
export declare const LINEAR_CHECKS: readonly [import("./check-kind.js").Check<{
|
|
4
|
+
title: string;
|
|
5
|
+
team: string;
|
|
6
|
+
}>, import("./check-kind.js").Check<{
|
|
7
|
+
title: string;
|
|
8
|
+
team: string;
|
|
9
|
+
state: string;
|
|
10
|
+
}>, import("./check-kind.js").Check<{
|
|
11
|
+
title: string;
|
|
12
|
+
team: string;
|
|
13
|
+
label: string;
|
|
14
|
+
}>, import("./check-kind.js").Check<{
|
|
15
|
+
title: string;
|
|
16
|
+
team: string;
|
|
17
|
+
estimate: string;
|
|
18
|
+
}>, import("./check-kind.js").Check<{
|
|
19
|
+
title: string;
|
|
20
|
+
team: string;
|
|
21
|
+
user: string;
|
|
22
|
+
}>, import("./check-kind.js").Check<{
|
|
23
|
+
title: string;
|
|
24
|
+
team: string;
|
|
25
|
+
needle: string;
|
|
26
|
+
}>, import("./check-kind.js").Check<{
|
|
27
|
+
title: string;
|
|
28
|
+
team: string;
|
|
29
|
+
}>, import("./check-kind.js").Check<Record<string, never>>];
|