@femtomc/mu-issue 26.2.68 → 26.2.70

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.
@@ -0,0 +1,33 @@
1
+ import type { Issue } from "@femtomc/mu-core";
2
+ export declare const ISSUE_STATUS_VALUES: readonly ["open", "in_progress", "closed"];
3
+ export declare const DEFAULT_ISSUE_QUERY_LIMIT = 200;
4
+ export declare const MAX_ISSUE_QUERY_LIMIT = 200;
5
+ export declare class IssueStoreError extends Error {
6
+ constructor(message: string, opts?: {
7
+ cause?: unknown;
8
+ });
9
+ }
10
+ export declare class IssueStoreNotFoundError extends IssueStoreError {
11
+ readonly issueId: string;
12
+ constructor(issueId: string);
13
+ }
14
+ export declare class IssueStoreValidationError extends IssueStoreError {
15
+ constructor(message: string, opts?: {
16
+ cause?: unknown;
17
+ });
18
+ }
19
+ export declare function normalizeIssueStatusFilter(status: unknown): Issue["status"] | undefined;
20
+ export declare function normalizeIssueTagFilter(tag: unknown): string | undefined;
21
+ export declare function normalizeIssueContainsFilter(contains: unknown): string | undefined;
22
+ export declare function normalizeIssueQueryLimit(limit: unknown, opts?: {
23
+ defaultLimit?: number | null;
24
+ max?: number;
25
+ }): number | null;
26
+ export declare function normalizeIssueDepInput(input: {
27
+ depType: unknown;
28
+ target: unknown;
29
+ }): {
30
+ depType: string;
31
+ target: string;
32
+ };
33
+ //# sourceMappingURL=contracts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contracts.d.ts","sourceRoot":"","sources":["../src/contracts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAE9C,eAAO,MAAM,mBAAmB,4CAAkF,CAAC;AACnH,eAAO,MAAM,yBAAyB,MAAM,CAAC;AAC7C,eAAO,MAAM,qBAAqB,MAAM,CAAC;AAIzC,qBAAa,eAAgB,SAAQ,KAAK;gBACtB,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAI9D;AAED,qBAAa,uBAAwB,SAAQ,eAAe;IAC3D,SAAgB,OAAO,EAAE,MAAM,CAAC;gBAEb,OAAO,EAAE,MAAM;CAKlC;AAED,qBAAa,yBAA0B,SAAQ,eAAe;gBAC1C,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAI9D;AAED,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,OAAO,GAAG,KAAK,CAAC,QAAQ,CAAC,GAAG,SAAS,CAevF;AAED,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CASxE;AAED,wBAAgB,4BAA4B,CAAC,QAAQ,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CASlF;AAED,wBAAgB,wBAAwB,CACvC,KAAK,EAAE,OAAO,EACd,IAAI,GAAE;IAAE,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAO,GACvD,MAAM,GAAG,IAAI,CAsBf;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,GAAG;IACrF,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CACf,CAUA"}
@@ -0,0 +1,93 @@
1
+ export const ISSUE_STATUS_VALUES = ["open", "in_progress", "closed"];
2
+ export const DEFAULT_ISSUE_QUERY_LIMIT = 200;
3
+ export const MAX_ISSUE_QUERY_LIMIT = 200;
4
+ const ISSUE_STATUS_SET = new Set(ISSUE_STATUS_VALUES);
5
+ export class IssueStoreError extends Error {
6
+ constructor(message, opts) {
7
+ super(message, opts);
8
+ this.name = "IssueStoreError";
9
+ }
10
+ }
11
+ export class IssueStoreNotFoundError extends IssueStoreError {
12
+ issueId;
13
+ constructor(issueId) {
14
+ super(`issue not found: ${issueId}`);
15
+ this.name = "IssueStoreNotFoundError";
16
+ this.issueId = issueId;
17
+ }
18
+ }
19
+ export class IssueStoreValidationError extends IssueStoreError {
20
+ constructor(message, opts) {
21
+ super(message, opts);
22
+ this.name = "IssueStoreValidationError";
23
+ }
24
+ }
25
+ export function normalizeIssueStatusFilter(status) {
26
+ if (status == null || status === "") {
27
+ return undefined;
28
+ }
29
+ if (typeof status !== "string") {
30
+ throw new IssueStoreValidationError("invalid issue status filter: expected string");
31
+ }
32
+ const trimmed = status.trim();
33
+ if (trimmed.length === 0) {
34
+ return undefined;
35
+ }
36
+ if (!ISSUE_STATUS_SET.has(trimmed)) {
37
+ throw new IssueStoreValidationError(`invalid issue status filter: ${trimmed}`);
38
+ }
39
+ return trimmed;
40
+ }
41
+ export function normalizeIssueTagFilter(tag) {
42
+ if (tag == null || tag === "") {
43
+ return undefined;
44
+ }
45
+ if (typeof tag !== "string") {
46
+ throw new IssueStoreValidationError("invalid issue tag filter: expected string");
47
+ }
48
+ const trimmed = tag.trim();
49
+ return trimmed.length > 0 ? trimmed : undefined;
50
+ }
51
+ export function normalizeIssueContainsFilter(contains) {
52
+ if (contains == null || contains === "") {
53
+ return undefined;
54
+ }
55
+ if (typeof contains !== "string") {
56
+ throw new IssueStoreValidationError("invalid issue contains filter: expected string");
57
+ }
58
+ const trimmed = contains.trim().toLowerCase();
59
+ return trimmed.length > 0 ? trimmed : undefined;
60
+ }
61
+ export function normalizeIssueQueryLimit(limit, opts = {}) {
62
+ const defaultLimit = opts.defaultLimit ?? null;
63
+ const max = opts.max ?? MAX_ISSUE_QUERY_LIMIT;
64
+ if (limit == null || limit === "") {
65
+ return defaultLimit;
66
+ }
67
+ let value;
68
+ if (typeof limit === "number" && Number.isFinite(limit)) {
69
+ value = limit;
70
+ }
71
+ else if (typeof limit === "string" && /^\d+$/.test(limit.trim())) {
72
+ value = Number.parseInt(limit, 10);
73
+ }
74
+ else {
75
+ throw new IssueStoreValidationError("invalid issue query limit: expected positive integer");
76
+ }
77
+ const normalized = Math.trunc(value);
78
+ if (normalized < 1) {
79
+ throw new IssueStoreValidationError("invalid issue query limit: must be >= 1");
80
+ }
81
+ return Math.min(max, normalized);
82
+ }
83
+ export function normalizeIssueDepInput(input) {
84
+ const depType = typeof input.depType === "string" ? input.depType.trim() : "";
85
+ if (depType.length === 0) {
86
+ throw new IssueStoreValidationError("dependency type is required");
87
+ }
88
+ const target = typeof input.target === "string" ? input.target.trim() : "";
89
+ if (target.length === 0) {
90
+ throw new IssueStoreValidationError("dependency target is required");
91
+ }
92
+ return { depType, target };
93
+ }
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
- export type { CreateIssueOpts, ListIssueOpts } from "./issue_store.js";
1
+ export type { CreateIssueOpts, ListIssueOpts, ReadyIssueOpts } from "./issue_store.js";
2
2
  export { IssueStore } from "./issue_store.js";
3
+ export { DEFAULT_ISSUE_QUERY_LIMIT, ISSUE_STATUS_VALUES, MAX_ISSUE_QUERY_LIMIT, IssueStoreError, IssueStoreNotFoundError, IssueStoreValidationError, normalizeIssueContainsFilter, normalizeIssueDepInput, normalizeIssueQueryLimit, normalizeIssueStatusFilter, normalizeIssueTagFilter, } from "./contracts.js";
3
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACvE,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,eAAe,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACvF,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EACN,yBAAyB,EACzB,mBAAmB,EACnB,qBAAqB,EACrB,eAAe,EACf,uBAAuB,EACvB,yBAAyB,EACzB,4BAA4B,EAC5B,sBAAsB,EACtB,wBAAwB,EACxB,0BAA0B,EAC1B,uBAAuB,GACvB,MAAM,gBAAgB,CAAC"}
package/dist/index.js CHANGED
@@ -1 +1,2 @@
1
1
  export { IssueStore } from "./issue_store.js";
2
+ export { DEFAULT_ISSUE_QUERY_LIMIT, ISSUE_STATUS_VALUES, MAX_ISSUE_QUERY_LIMIT, IssueStoreError, IssueStoreNotFoundError, IssueStoreValidationError, normalizeIssueContainsFilter, normalizeIssueDepInput, normalizeIssueQueryLimit, normalizeIssueStatusFilter, normalizeIssueTagFilter, } from "./contracts.js";
@@ -6,8 +6,15 @@ export type CreateIssueOpts = {
6
6
  priority?: number;
7
7
  };
8
8
  export type ListIssueOpts = {
9
- status?: Issue["status"];
10
- tag?: string;
9
+ status?: Issue["status"] | null;
10
+ tag?: string | null;
11
+ contains?: string | null;
12
+ limit?: number | null;
13
+ };
14
+ export type ReadyIssueOpts = {
15
+ tags?: readonly string[] | null;
16
+ contains?: string | null;
17
+ limit?: number | null;
11
18
  };
12
19
  export declare class IssueStore {
13
20
  #private;
@@ -26,9 +33,7 @@ export declare class IssueStore {
26
33
  remove_dep(srcId: string, depType: string, dstId: string): Promise<boolean>;
27
34
  children(parentId: string): Promise<Issue[]>;
28
35
  subtree_ids(rootId: string): Promise<string[]>;
29
- ready(rootId?: string | null, opts?: {
30
- tags?: readonly string[] | null;
31
- }): Promise<Issue[]>;
36
+ ready(rootId?: string | null, opts?: ReadyIssueOpts): Promise<Issue[]>;
32
37
  collapsible(rootId: string): Promise<Issue[]>;
33
38
  validate(rootId: string): Promise<ValidationResult>;
34
39
  }
@@ -1 +1 @@
1
- {"version":3,"file":"issue_store.d.ts","sourceRoot":"","sources":["../src/issue_store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAC5E,OAAO,EAGN,QAAQ,EAOR,MAAM,kBAAkB,CAAC;AAE1B,MAAM,MAAM,eAAe,GAAG;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC3B,MAAM,CAAC,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;CACb,CAAC;AAgDF,qBAAa,UAAU;;IACtB,SAAgB,MAAM,EAAE,QAAQ,CAAC;gBAGd,MAAM,EAAE,UAAU,CAAC,OAAO,CAAC,EAAE,IAAI,GAAE;QAAE,MAAM,CAAC,EAAE,QAAQ,CAAA;KAAO;IAwBnE,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,GAAE,eAAoB,GAAG,OAAO,CAAC,KAAK,CAAC;IA4BjE,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC;IAM3C,IAAI,CAAC,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC;IAWhD,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC;IA0ExE,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IA+BxC,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,MAAkB,GAAG,OAAO,CAAC,KAAK,CAAC;IAInE,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAmBpD,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAyBrE,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IA2B3E,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC;IAK5C,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAS9C,KAAK,CAAC,MAAM,GAAE,MAAM,GAAG,IAAW,EAAE,IAAI,GAAE;QAAE,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAAA;KAAO,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC;IAOrG,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC;IAK7C,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;CAIhE"}
1
+ {"version":3,"file":"issue_store.d.ts","sourceRoot":"","sources":["../src/issue_store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAC5E,OAAO,EAGN,QAAQ,EAOR,MAAM,kBAAkB,CAAC;AAW1B,MAAM,MAAM,eAAe,GAAG;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC3B,MAAM,CAAC,EAAE,KAAK,CAAC,QAAQ,CAAC,GAAG,IAAI,CAAC;IAChC,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC5B,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAAC;IAChC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB,CAAC;AAqDF,qBAAa,UAAU;;IACtB,SAAgB,MAAM,EAAE,QAAQ,CAAC;gBAGd,MAAM,EAAE,UAAU,CAAC,OAAO,CAAC,EAAE,IAAI,GAAE;QAAE,MAAM,CAAC,EAAE,QAAQ,CAAA;KAAO;IA0CnE,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,GAAE,eAAoB,GAAG,OAAO,CAAC,KAAK,CAAC;IA0BjE,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC;IAM3C,IAAI,CAAC,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC;IA0ChD,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC;IA8ExE,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IA+BxC,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,MAAkB,GAAG,OAAO,CAAC,KAAK,CAAC;IAInE,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAmBpD,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAuCrE,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAmC3E,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC;IAK5C,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAS9C,KAAK,CAAC,MAAM,GAAE,MAAM,GAAG,IAAW,EAAE,IAAI,GAAE,cAAmB,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC;IAgBhF,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC;IAK7C,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;CAIhE"}
@@ -1,4 +1,5 @@
1
1
  import { collapsible as dagCollapsible, subtreeIds as dagSubtreeIds, EventLog, IssueSchema, NullEventSink, nowTs, readyLeaves, shortId, validateDag, } from "@femtomc/mu-core";
2
+ import { IssueStoreNotFoundError, IssueStoreValidationError, normalizeIssueContainsFilter, normalizeIssueDepInput, normalizeIssueQueryLimit, normalizeIssueStatusFilter, normalizeIssueTagFilter, } from "./contracts.js";
2
3
  function deepEqualJson(a, b) {
3
4
  if (a === b) {
4
5
  return true;
@@ -44,6 +45,10 @@ function deepEqualJson(a, b) {
44
45
  }
45
46
  return false;
46
47
  }
48
+ function issueContainsText(issue, contains) {
49
+ const haystack = `${issue.title}\n${issue.body}`.toLowerCase();
50
+ return haystack.includes(contains);
51
+ }
47
52
  export class IssueStore {
48
53
  events;
49
54
  #issues;
@@ -51,15 +56,30 @@ export class IssueStore {
51
56
  this.#issues = issues;
52
57
  this.events = opts.events ?? new EventLog(new NullEventSink());
53
58
  }
59
+ #parseIssueRow(row, idx) {
60
+ const parsed = IssueSchema.safeParse(row);
61
+ if (!parsed.success) {
62
+ throw new Error(`invalid issue row ${idx}: ${parsed.error.message}`);
63
+ }
64
+ return parsed.data;
65
+ }
54
66
  async #load() {
55
67
  const rows = await this.#issues.read();
56
- return rows.map((row, idx) => {
57
- const parsed = IssueSchema.safeParse(row);
58
- if (!parsed.success) {
59
- throw new Error(`invalid issue row ${idx}: ${parsed.error.message}`);
68
+ return rows.map((row, idx) => this.#parseIssueRow(row, idx));
69
+ }
70
+ async *#streamRows() {
71
+ if (this.#issues.stream) {
72
+ let idx = 0;
73
+ for await (const row of this.#issues.stream()) {
74
+ yield this.#parseIssueRow(row, idx);
75
+ idx += 1;
60
76
  }
61
- return parsed.data;
62
- });
77
+ return;
78
+ }
79
+ const rows = await this.#issues.read();
80
+ for (let idx = 0; idx < rows.length; idx += 1) {
81
+ yield this.#parseIssueRow(rows[idx], idx);
82
+ }
63
83
  }
64
84
  async #save(rows) {
65
85
  await this.#issues.write(rows);
@@ -82,9 +102,7 @@ export class IssueStore {
82
102
  updated_at: now,
83
103
  };
84
104
  const issue = IssueSchema.parse(issueInput);
85
- const rows = await this.#load();
86
- rows.push(issue);
87
- await this.#save(rows);
105
+ await this.#issues.append(issue);
88
106
  await this.events.emit("issue.create", {
89
107
  source: "issue_store",
90
108
  issueId: issue.id,
@@ -98,20 +116,52 @@ export class IssueStore {
98
116
  return idx >= 0 ? rows[idx] : null;
99
117
  }
100
118
  async list(opts = {}) {
119
+ const status = normalizeIssueStatusFilter(opts.status);
120
+ const tag = normalizeIssueTagFilter(opts.tag);
121
+ const contains = normalizeIssueContainsFilter(opts.contains);
122
+ const limit = normalizeIssueQueryLimit(opts.limit, { defaultLimit: null });
123
+ if (limit != null && this.#issues.stream) {
124
+ const bounded = [];
125
+ for await (const row of this.#streamRows()) {
126
+ if (status && row.status !== status) {
127
+ continue;
128
+ }
129
+ if (tag && !row.tags.includes(tag)) {
130
+ continue;
131
+ }
132
+ if (contains && !issueContainsText(row, contains)) {
133
+ continue;
134
+ }
135
+ bounded.push(row);
136
+ if (bounded.length > limit) {
137
+ bounded.shift();
138
+ }
139
+ }
140
+ return bounded;
141
+ }
101
142
  let rows = await this.#load();
102
- if (opts.status) {
103
- rows = rows.filter((row) => row.status === opts.status);
143
+ if (status) {
144
+ rows = rows.filter((row) => row.status === status);
145
+ }
146
+ if (tag) {
147
+ rows = rows.filter((row) => row.tags.includes(tag));
104
148
  }
105
- if (opts.tag) {
106
- rows = rows.filter((row) => row.tags.includes(opts.tag));
149
+ if (contains) {
150
+ rows = rows.filter((row) => issueContainsText(row, contains));
151
+ }
152
+ if (limit != null) {
153
+ rows = rows.slice(-limit);
107
154
  }
108
155
  return rows;
109
156
  }
110
157
  async update(issueId, fields) {
158
+ if (!fields || typeof fields !== "object" || Array.isArray(fields)) {
159
+ throw new IssueStoreValidationError("update fields must be an object");
160
+ }
111
161
  const rows = await this.#load();
112
162
  const idx = this.#findIndex(rows, issueId);
113
163
  if (idx < 0) {
114
- throw new Error(issueId);
164
+ throw new IssueStoreNotFoundError(issueId);
115
165
  }
116
166
  const issueBefore = rows[idx];
117
167
  const before = JSON.parse(JSON.stringify(issueBefore));
@@ -221,35 +271,51 @@ export class IssueStore {
221
271
  return reset;
222
272
  }
223
273
  async add_dep(srcId, depType, dstId) {
274
+ const normalizedSrcId = srcId.trim();
275
+ if (normalizedSrcId.length === 0) {
276
+ throw new IssueStoreValidationError("source issue id is required");
277
+ }
278
+ const normalizedDep = normalizeIssueDepInput({ depType, target: dstId });
279
+ if (normalizedDep.target === normalizedSrcId) {
280
+ throw new IssueStoreValidationError("dependency target cannot equal source issue id");
281
+ }
224
282
  const rows = await this.#load();
225
- const idx = this.#findIndex(rows, srcId);
283
+ const idx = this.#findIndex(rows, normalizedSrcId);
226
284
  if (idx < 0) {
227
- throw new Error(srcId);
285
+ throw new IssueStoreNotFoundError(normalizedSrcId);
286
+ }
287
+ if (this.#findIndex(rows, normalizedDep.target) < 0) {
288
+ throw new IssueStoreNotFoundError(normalizedDep.target);
228
289
  }
229
290
  const issue = rows[idx];
230
- const exists = issue.deps.some((dep) => dep.type === depType && dep.target === dstId);
291
+ const exists = issue.deps.some((dep) => dep.type === normalizedDep.depType && dep.target === normalizedDep.target);
231
292
  if (exists) {
232
293
  return;
233
294
  }
234
- issue.deps.push({ type: depType, target: dstId });
295
+ issue.deps.push({ type: normalizedDep.depType, target: normalizedDep.target });
235
296
  issue.updated_at = nowTs();
236
297
  rows[idx] = IssueSchema.parse(issue);
237
298
  await this.#save(rows);
238
299
  await this.events.emit("issue.dep.add", {
239
300
  source: "issue_store",
240
- issueId: srcId,
241
- payload: { type: depType, target: dstId },
301
+ issueId: normalizedSrcId,
302
+ payload: { type: normalizedDep.depType, target: normalizedDep.target },
242
303
  });
243
304
  }
244
305
  async remove_dep(srcId, depType, dstId) {
306
+ const normalizedSrcId = srcId.trim();
307
+ if (normalizedSrcId.length === 0) {
308
+ throw new IssueStoreValidationError("source issue id is required");
309
+ }
310
+ const normalizedDep = normalizeIssueDepInput({ depType, target: dstId });
245
311
  const rows = await this.#load();
246
- const idx = this.#findIndex(rows, srcId);
312
+ const idx = this.#findIndex(rows, normalizedSrcId);
247
313
  if (idx < 0) {
248
- throw new Error(srcId);
314
+ throw new IssueStoreNotFoundError(normalizedSrcId);
249
315
  }
250
316
  const issue = rows[idx];
251
317
  const before = issue.deps.length;
252
- issue.deps = issue.deps.filter((dep) => !(dep.type === depType && dep.target === dstId));
318
+ issue.deps = issue.deps.filter((dep) => !(dep.type === normalizedDep.depType && dep.target === normalizedDep.target));
253
319
  const changed = issue.deps.length !== before;
254
320
  if (changed) {
255
321
  issue.updated_at = nowTs();
@@ -258,8 +324,8 @@ export class IssueStore {
258
324
  }
259
325
  await this.events.emit("issue.dep.remove", {
260
326
  source: "issue_store",
261
- issueId: srcId,
262
- payload: { type: depType, target: dstId, ok: changed },
327
+ issueId: normalizedSrcId,
328
+ payload: { type: normalizedDep.depType, target: normalizedDep.target, ok: changed },
263
329
  });
264
330
  return changed;
265
331
  }
@@ -277,8 +343,17 @@ export class IssueStore {
277
343
  async ready(rootId = null, opts = {}) {
278
344
  const rows = await this.#load();
279
345
  const tags = opts.tags ?? undefined;
346
+ const contains = normalizeIssueContainsFilter(opts.contains);
347
+ const limit = normalizeIssueQueryLimit(opts.limit, { defaultLimit: null });
280
348
  const root_id = rootId ?? undefined;
281
- return readyLeaves(rows, { root_id, tags: tags ?? undefined });
349
+ let ready = readyLeaves(rows, { root_id, tags: tags ?? undefined });
350
+ if (contains) {
351
+ ready = ready.filter((issue) => issueContainsText(issue, contains));
352
+ }
353
+ if (limit != null) {
354
+ ready = ready.slice(0, limit);
355
+ }
356
+ return ready;
282
357
  }
283
358
  async collapsible(rootId) {
284
359
  const rows = await this.#load();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@femtomc/mu-issue",
3
- "version": "26.2.68",
3
+ "version": "26.2.70",
4
4
  "description": "Work item store with dependency graph helpers for mu.",
5
5
  "keywords": [
6
6
  "mu",
@@ -22,6 +22,6 @@
22
22
  "dist/**"
23
23
  ],
24
24
  "dependencies": {
25
- "@femtomc/mu-core": "26.2.68"
25
+ "@femtomc/mu-core": "26.2.70"
26
26
  }
27
27
  }