codetrap 0.1.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.
@@ -0,0 +1,272 @@
1
+ import { openGlobal, openProject } from "../db/connection";
2
+ import { TrapRepository, type TrapStats } from "../db/repository";
3
+ import {
4
+ type Trap,
5
+ type TrapDetails,
6
+ type TrapEvidenceInput,
7
+ type TrapExportRecord,
8
+ type TrapImportRecord,
9
+ type TrapInput,
10
+ type TrapSearchResult,
11
+ type TrapUpdate,
12
+ } from "../domain/trap";
13
+ import type { SearchMode, TrapStatus } from "./constants";
14
+ import { createDefaultEmbeddingProvider, type EmbeddingProvider } from "./embedder";
15
+ import { findProjectRoot } from "./scope";
16
+ import { importTrapArchive } from "./trap-archive";
17
+
18
+ export {
19
+ type TrapInput,
20
+ type TrapSearchResult,
21
+ type Trap,
22
+ type TrapDetails,
23
+ type TrapExportRecord,
24
+ type TrapImportRecord,
25
+ type TrapUpdate,
26
+ type TrapStats,
27
+ };
28
+
29
+ type Scope = "project" | "global";
30
+
31
+ export class TrapStore {
32
+ private projectRoot: string | null;
33
+ private globalRepository?: TrapRepository;
34
+ private projectRepository?: TrapRepository;
35
+ private readonly embedder?: EmbeddingProvider;
36
+
37
+ constructor(cwd: string, embedder: EmbeddingProvider | undefined = createDefaultEmbeddingProvider()) {
38
+ this.projectRoot = findProjectRoot(cwd);
39
+ this.embedder = embedder;
40
+ }
41
+
42
+ add(input: TrapInput): { id: number; scope: string } {
43
+ const scope = normalizeScope(input.scope);
44
+ if (scope === "project" && !this.projectRoot) {
45
+ throw new Error("Not in a project. Run 'codetrap init' first, or use --scope global.");
46
+ }
47
+
48
+ const id = this.repositoryFor(scope).add({
49
+ ...input,
50
+ scope,
51
+ project_path: scope === "project" ? this.projectRoot : null,
52
+ });
53
+ return { id, scope };
54
+ }
55
+
56
+ async search(
57
+ query: string,
58
+ opts: { category?: string; scope?: string; limit?: number; mode?: SearchMode; status?: TrapStatus | "all" } = {}
59
+ ): Promise<{ results: TrapSearchResult[]; scope: string }[]> {
60
+ const out: { results: TrapSearchResult[]; scope: string }[] = [];
61
+ const limit = opts.limit ?? 20;
62
+
63
+ for (const scoped of this.repositoriesForRead(opts.scope)) {
64
+ const results = await scoped.repository.search(query, {
65
+ category: opts.category,
66
+ scope: scoped.scope,
67
+ limit,
68
+ mode: opts.mode ?? "hybrid",
69
+ status: opts.status,
70
+ });
71
+ if (results.length > 0) out.push({ results, scope: scoped.scope });
72
+ }
73
+
74
+ return out;
75
+ }
76
+
77
+ get(id: number, scope?: string): { trap: Trap; scope: string } | null {
78
+ for (const scoped of this.repositoriesForRead(scope)) {
79
+ const trap = scoped.repository.get(id);
80
+ if (trap) return { trap, scope: scoped.scope };
81
+ }
82
+ return null;
83
+ }
84
+
85
+ getDetails(id: number, scope?: string): TrapDetails | null {
86
+ for (const scoped of this.repositoriesForRead(scope)) {
87
+ const details = scoped.repository.getDetails(id, scoped.scope);
88
+ if (details) return details;
89
+ }
90
+ return null;
91
+ }
92
+
93
+ list(opts: { category?: string; scope?: string; limit?: number; offset?: number; status?: TrapStatus | "all" } = {}): { traps: Trap[]; scope: string }[] {
94
+ const out: { traps: Trap[]; scope: string }[] = [];
95
+
96
+ for (const scoped of this.repositoriesForRead(opts.scope)) {
97
+ const traps = scoped.repository.list({ ...opts, scope: scoped.scope });
98
+ if (traps.length > 0) out.push({ traps, scope: scoped.scope });
99
+ }
100
+
101
+ return out;
102
+ }
103
+
104
+ update(id: number, input: TrapUpdate, scope?: string): { scope: string; success: boolean } {
105
+ for (const scoped of this.repositoriesForWrite(scope)) {
106
+ const success = scoped.repository.update(id, input);
107
+ if (success || scope) return { scope: scoped.scope, success };
108
+ }
109
+ return { scope: "global", success: false };
110
+ }
111
+
112
+ delete(id: number, scope?: string): { scope: string; success: boolean } {
113
+ for (const scoped of this.repositoriesForWrite(scope)) {
114
+ const success = scoped.repository.delete(id);
115
+ if (success || scope) return { scope: scoped.scope, success };
116
+ }
117
+ return { scope: "global", success: false };
118
+ }
119
+
120
+ addEvidence(id: number, input: TrapEvidenceInput, scope?: string): { scope: string; evidence_id: number | null; success: boolean } {
121
+ for (const scoped of this.repositoriesForWrite(scope)) {
122
+ const evidenceId = scoped.repository.addEvidence(id, input);
123
+ if (evidenceId || scope) return { scope: scoped.scope, evidence_id: evidenceId, success: evidenceId !== null };
124
+ }
125
+ return { scope: "global", evidence_id: null, success: false };
126
+ }
127
+
128
+ archive(id: number, scope?: string): { scope: string; success: boolean } {
129
+ for (const scoped of this.repositoriesForWrite(scope)) {
130
+ const success = scoped.repository.archive(id);
131
+ if (success || scope) return { scope: scoped.scope, success };
132
+ }
133
+ return { scope: "global", success: false };
134
+ }
135
+
136
+ supersede(
137
+ id: number,
138
+ supersededById: number,
139
+ scope?: string,
140
+ stateKey?: string
141
+ ): { scope: string; success: boolean } {
142
+ for (const scoped of this.repositoriesForWrite(scope)) {
143
+ const success = scoped.repository.supersede(id, supersededById, stateKey);
144
+ if (success || scope) return { scope: scoped.scope, success };
145
+ }
146
+ return { scope: "global", success: false };
147
+ }
148
+
149
+ hit(id: number, scope?: string): void {
150
+ for (const scoped of this.repositoriesForRead(scope)) {
151
+ if (scope || scoped.repository.get(id)) {
152
+ scoped.repository.hit(id);
153
+ return;
154
+ }
155
+ }
156
+ }
157
+
158
+ topTraps(scope: string, limit = 20): Trap[] {
159
+ const resolvedScope = normalizeScope(scope);
160
+ return this.repositoryFor(resolvedScope).top(resolvedScope, limit);
161
+ }
162
+
163
+ stats(): { project: TrapStats | null; global: TrapStats } {
164
+ const project = this.projectRoot ? this.projectRepo().stats() : null;
165
+ return { project, global: this.globalRepo().stats() };
166
+ }
167
+
168
+ exportAll(opts: { scope?: string } = {}): TrapExportRecord[] {
169
+ const traps: TrapExportRecord[] = [];
170
+ for (const scoped of this.repositoriesForRead(opts.scope)) {
171
+ traps.push(...scoped.repository.exportAll());
172
+ }
173
+ return traps;
174
+ }
175
+
176
+ importAll(traps: TrapImportRecord[]): number {
177
+ return importTrapArchive(traps, {
178
+ add: (input) => this.add(input),
179
+ addEvidence: (id, input, scope) => this.addEvidence(id, input, scope),
180
+ });
181
+ }
182
+
183
+ hasProject(): boolean {
184
+ return this.projectRoot !== null;
185
+ }
186
+
187
+ getProjectRoot(): string | null {
188
+ return this.projectRoot;
189
+ }
190
+
191
+ async ensureEmbeddings(opts: { scope?: string; category?: string; limit?: number; force?: boolean; batchSize?: number } = {}): Promise<{
192
+ generated: number;
193
+ skipped: number;
194
+ batches: number;
195
+ scopes: { scope: string; generated: number; skipped: number; batches: number }[];
196
+ }> {
197
+ const scopes: { scope: string; generated: number; skipped: number; batches: number }[] = [];
198
+ let generated = 0;
199
+ let skipped = 0;
200
+ let batches = 0;
201
+
202
+ for (const scoped of this.repositoriesForRead(opts.scope)) {
203
+ const result = await scoped.repository.ensureEmbeddings({
204
+ scope: scoped.scope,
205
+ category: opts.category,
206
+ limit: opts.limit,
207
+ force: opts.force,
208
+ batchSize: opts.batchSize,
209
+ });
210
+ scopes.push({ scope: scoped.scope, ...result });
211
+ generated += result.generated;
212
+ skipped += result.skipped;
213
+ batches += result.batches;
214
+ }
215
+
216
+ return { generated, skipped, batches, scopes };
217
+ }
218
+
219
+ private repositoriesForRead(scope?: string): { scope: Scope; repository: TrapRepository }[] {
220
+ const resolvedScope = optionalScope(scope);
221
+ if (resolvedScope) return this.repositoryEntry(resolvedScope) ? [this.repositoryEntry(resolvedScope)!] : [];
222
+
223
+ const repositories: { scope: Scope; repository: TrapRepository }[] = [];
224
+ const project = this.repositoryEntry("project");
225
+ if (project) repositories.push(project);
226
+ repositories.push({ scope: "global", repository: this.globalRepo() });
227
+ return repositories;
228
+ }
229
+
230
+ private repositoriesForWrite(scope?: string): { scope: Scope; repository: TrapRepository }[] {
231
+ return this.repositoriesForRead(scope);
232
+ }
233
+
234
+ private repositoryEntry(scope: Scope): { scope: Scope; repository: TrapRepository } | null {
235
+ if (scope === "project") {
236
+ return this.projectRoot ? { scope, repository: this.projectRepo() } : null;
237
+ }
238
+ return { scope, repository: this.globalRepo() };
239
+ }
240
+
241
+ private repositoryFor(scope: Scope): TrapRepository {
242
+ if (scope === "project") return this.projectRepo();
243
+ return this.globalRepo();
244
+ }
245
+
246
+ private projectRepo(): TrapRepository {
247
+ if (!this.projectRoot) {
248
+ throw new Error("Not in a project. Run 'codetrap init' first, or use --scope global.");
249
+ }
250
+ if (!this.projectRepository) {
251
+ this.projectRepository = new TrapRepository(openProject(this.projectRoot), this.embedder);
252
+ }
253
+ return this.projectRepository;
254
+ }
255
+
256
+ private globalRepo(): TrapRepository {
257
+ if (!this.globalRepository) {
258
+ this.globalRepository = new TrapRepository(openGlobal(), this.embedder);
259
+ }
260
+ return this.globalRepository;
261
+ }
262
+ }
263
+
264
+ function normalizeScope(scope: string): Scope {
265
+ if (scope === "project" || scope === "global") return scope;
266
+ throw new Error(`Invalid scope: ${scope}`);
267
+ }
268
+
269
+ function optionalScope(scope?: string): Scope | null {
270
+ if (!scope) return null;
271
+ return normalizeScope(scope);
272
+ }
@@ -0,0 +1,91 @@
1
+ import {
2
+ buildTrapEvidenceInput,
3
+ type TrapEvidence,
4
+ type TrapEvidenceInput,
5
+ type TrapExportEvidence,
6
+ type TrapImportEvidence,
7
+ type TrapImportRecord,
8
+ type TrapInput,
9
+ } from "../domain/trap";
10
+ import {
11
+ parseEvidenceRelatedFiles,
12
+ parseOptionalEvidenceRelatedFiles,
13
+ parseOptionalTrapTags,
14
+ } from "./trap-json-fields";
15
+
16
+ export interface TrapArchiveImportAdapter {
17
+ add(input: TrapInput): { id: number; scope: string };
18
+ addEvidence(
19
+ id: number,
20
+ input: TrapEvidenceInput,
21
+ scope?: string
22
+ ): { scope: string; evidence_id: number | null; success: boolean };
23
+ }
24
+
25
+ export function normalizeEvidenceForExport(evidence: TrapEvidence): TrapExportEvidence {
26
+ return {
27
+ ...evidence,
28
+ related_files: parseEvidenceRelatedFiles(evidence.related_files),
29
+ };
30
+ }
31
+
32
+ export function importTrapArchive(
33
+ records: TrapImportRecord[],
34
+ adapter: TrapArchiveImportAdapter
35
+ ): number {
36
+ let count = 0;
37
+ for (const record of records) {
38
+ try {
39
+ const result = adapter.add(toTrapInput(record));
40
+ importEvidenceRecords(record.evidence ?? [], result, adapter);
41
+ count++;
42
+ } catch {
43
+ // Preserve legacy behavior: import valid traps even if another trap is malformed.
44
+ }
45
+ }
46
+ return count;
47
+ }
48
+
49
+ function importEvidenceRecords(
50
+ records: TrapImportEvidence[],
51
+ trap: { id: number; scope: string },
52
+ adapter: TrapArchiveImportAdapter
53
+ ): void {
54
+ for (const evidence of records) {
55
+ try {
56
+ adapter.addEvidence(
57
+ trap.id,
58
+ buildTrapEvidenceInput(toEvidenceInput(evidence)),
59
+ trap.scope
60
+ );
61
+ } catch {
62
+ // Preserve valid trap imports even if one evidence record is malformed.
63
+ }
64
+ }
65
+ }
66
+
67
+ function toTrapInput(record: TrapImportRecord): TrapInput {
68
+ return {
69
+ title: record.title,
70
+ category: record.category,
71
+ tags: parseOptionalTrapTags(record.tags),
72
+ scope: record.scope,
73
+ context: record.context,
74
+ mistake: record.mistake,
75
+ fix: record.fix,
76
+ before_code: record.before_code ?? undefined,
77
+ after_code: record.after_code ?? undefined,
78
+ severity: record.severity ?? undefined,
79
+ project_path: record.project_path ?? undefined,
80
+ };
81
+ }
82
+
83
+ function toEvidenceInput(evidence: TrapImportEvidence): Record<string, unknown> {
84
+ return {
85
+ source_type: evidence.source_type,
86
+ source_ref: evidence.source_ref ?? undefined,
87
+ observed_at: evidence.observed_at ?? undefined,
88
+ related_files: parseOptionalEvidenceRelatedFiles(evidence.related_files),
89
+ note: evidence.note ?? undefined,
90
+ };
91
+ }
@@ -0,0 +1,42 @@
1
+ export type TrapStringArrayInput = string[] | string | null | undefined;
2
+
3
+ export function parseTrapTags(value: TrapStringArrayInput): string[] {
4
+ return parseStringArrayField(value);
5
+ }
6
+
7
+ export function parseOptionalTrapTags(value: TrapStringArrayInput): string[] | undefined {
8
+ return emptyToUndefined(parseTrapTags(value));
9
+ }
10
+
11
+ export function encodeTrapTags(value: TrapStringArrayInput): string {
12
+ return JSON.stringify(parseTrapTags(value));
13
+ }
14
+
15
+ export function parseEvidenceRelatedFiles(value: TrapStringArrayInput): string[] {
16
+ return parseStringArrayField(value);
17
+ }
18
+
19
+ export function parseOptionalEvidenceRelatedFiles(value: TrapStringArrayInput): string[] | undefined {
20
+ return emptyToUndefined(parseEvidenceRelatedFiles(value));
21
+ }
22
+
23
+ export function encodeEvidenceRelatedFiles(value: TrapStringArrayInput): string {
24
+ return JSON.stringify(parseEvidenceRelatedFiles(value));
25
+ }
26
+
27
+ function parseStringArrayField(value: TrapStringArrayInput): string[] {
28
+ if (Array.isArray(value)) return value.map(String);
29
+ if (value === null || value === undefined || value === "") return [];
30
+
31
+ try {
32
+ const parsed = JSON.parse(value);
33
+ if (Array.isArray(parsed)) return parsed.map(String);
34
+ return typeof parsed === "string" && parsed !== "" ? [parsed] : [];
35
+ } catch {
36
+ return [value];
37
+ }
38
+ }
39
+
40
+ function emptyToUndefined(values: string[]): string[] | undefined {
41
+ return values.length > 0 ? values : undefined;
42
+ }
@@ -0,0 +1,127 @@
1
+ import {
2
+ buildTrapEvidenceInput,
3
+ buildTrapInput,
4
+ parseTrapStatus,
5
+ pickTrapUpdate,
6
+ type Trap,
7
+ type TrapActionCard,
8
+ type TrapDetails,
9
+ type TrapImportRecord,
10
+ type TrapSearchResult,
11
+ } from "../domain/trap";
12
+ import type { TrapStore, TrapStats } from "./store";
13
+ import type { SearchMode } from "./constants";
14
+ import { toTrapActionCards } from "./search-result-card";
15
+
16
+ export type TrapListGroup = { traps: Trap[]; scope: string };
17
+ export type TrapSearchGroup = { results: TrapSearchResult[]; scope: string };
18
+ export type TrapMutationResult = { scope: string; success: boolean };
19
+ export type AddTrapEvidenceResult = {
20
+ scope: string;
21
+ evidence_id: number | null;
22
+ success: boolean;
23
+ };
24
+
25
+ export type TrapStatsResult = { project: TrapStats | null; global: TrapStats };
26
+
27
+ export interface SearchTrapsArgs {
28
+ query: string;
29
+ category?: string;
30
+ scope?: string;
31
+ limit?: number;
32
+ mode?: SearchMode;
33
+ status?: string;
34
+ }
35
+
36
+ export interface ListTrapsArgs {
37
+ category?: string;
38
+ scope?: string;
39
+ limit?: number;
40
+ offset?: number;
41
+ status?: string;
42
+ }
43
+
44
+ export class TrapOperations {
45
+ constructor(private readonly store: TrapStore) {}
46
+
47
+ addTrap(args: Record<string, unknown>): { id: number; scope: string } {
48
+ return this.store.add(buildTrapInput(args));
49
+ }
50
+
51
+ async searchTrapGroups(args: SearchTrapsArgs): Promise<TrapSearchGroup[]> {
52
+ return this.store.search(args.query, {
53
+ category: args.category,
54
+ scope: args.scope,
55
+ limit: args.limit ?? 20,
56
+ mode: args.mode,
57
+ status: parseTrapStatus(args.status),
58
+ });
59
+ }
60
+
61
+ async searchTrapCards(args: SearchTrapsArgs): Promise<TrapActionCard[]> {
62
+ return toTrapActionCards(await this.searchTrapGroups(args));
63
+ }
64
+
65
+ getTrapDetails(id: number, scope?: string): TrapDetails | null {
66
+ return this.store.getDetails(id, scope);
67
+ }
68
+
69
+ hitTrap(id: number, scope?: string): void {
70
+ this.store.hit(id, scope);
71
+ }
72
+
73
+ listTraps(args: ListTrapsArgs = {}): TrapListGroup[] {
74
+ return this.store.list({
75
+ category: args.category,
76
+ scope: args.scope,
77
+ status: parseTrapStatus(args.status),
78
+ limit: args.limit ?? 50,
79
+ offset: args.offset,
80
+ });
81
+ }
82
+
83
+ updateTrap(
84
+ id: number,
85
+ args: Record<string, unknown>,
86
+ scope?: string
87
+ ): TrapMutationResult {
88
+ return this.store.update(id, pickTrapUpdate(args), scope);
89
+ }
90
+
91
+ deleteTrap(id: number, scope?: string): TrapMutationResult {
92
+ return this.store.delete(id, scope);
93
+ }
94
+
95
+ addTrapEvidence(
96
+ id: number,
97
+ args: Record<string, unknown>,
98
+ scope?: string
99
+ ): AddTrapEvidenceResult {
100
+ return this.store.addEvidence(id, buildTrapEvidenceInput(args), scope);
101
+ }
102
+
103
+ archiveTrap(id: number, scope?: string): TrapMutationResult {
104
+ return this.store.archive(id, scope);
105
+ }
106
+
107
+ supersedeTrap(
108
+ id: number,
109
+ supersededById: number,
110
+ scope?: string,
111
+ stateKey?: string
112
+ ): TrapMutationResult {
113
+ return this.store.supersede(id, supersededById, scope, stateKey);
114
+ }
115
+
116
+ getStats(): TrapStatsResult {
117
+ return this.store.stats();
118
+ }
119
+
120
+ exportTraps(scope?: string): ReturnType<TrapStore["exportAll"]> {
121
+ return this.store.exportAll({ scope });
122
+ }
123
+
124
+ importTraps(records: TrapImportRecord[]): number {
125
+ return this.store.importAll(records);
126
+ }
127
+ }
@@ -0,0 +1,73 @@
1
+ import { createHash } from "node:crypto";
2
+ import type { Trap, TrapInput, TrapUpdate } from "../domain/trap";
3
+ import type { EmbeddingConfig, StoredEmbedding } from "./embedder";
4
+ import { buildSearchText, SEARCH_TEXT_FIELD_NAMES, type SearchTextFields } from "./search-normalizer";
5
+ import { parseTrapTags } from "./trap-json-fields";
6
+
7
+ export const PASSAGE_VERSION = 1;
8
+
9
+ export const PASSAGE_FIELD_NAMES = [
10
+ "title",
11
+ "category",
12
+ "context",
13
+ "mistake",
14
+ "fix",
15
+ "tags",
16
+ "before_code",
17
+ "after_code",
18
+ "severity",
19
+ ] as const;
20
+
21
+ export type TrapSearchDocumentFields = SearchTextFields & {
22
+ title?: string;
23
+ category?: string;
24
+ severity?: string;
25
+ };
26
+
27
+ export function buildTrapSearchText(fields: SearchTextFields): string {
28
+ return buildSearchText(fields);
29
+ }
30
+
31
+ export function buildTrapPassage(trap: TrapSearchDocumentFields): string {
32
+ const tags = parseTrapTags(trap.tags).join(", ");
33
+ return [
34
+ trap.title ? `Title: ${trap.title}` : "",
35
+ trap.category ? `Category: ${trap.category}` : "",
36
+ trap.severity ? `Severity: ${trap.severity}` : "",
37
+ tags ? `Tags: ${tags}` : "",
38
+ trap.context ? `Context: ${trap.context}` : "",
39
+ trap.mistake ? `Mistake: ${trap.mistake}` : "",
40
+ trap.fix ? `Fix: ${trap.fix}` : "",
41
+ trap.before_code ? `Before code:\n${trap.before_code}` : "",
42
+ trap.after_code ? `After code:\n${trap.after_code}` : "",
43
+ ]
44
+ .filter(Boolean)
45
+ .join("\n\n");
46
+ }
47
+
48
+ export function hashTrapPassage(passage: string): string {
49
+ return createHash("sha256").update(passage).digest("hex");
50
+ }
51
+
52
+ export function passageHashForTrap(trap: TrapSearchDocumentFields): string {
53
+ return hashTrapPassage(buildTrapPassage(trap));
54
+ }
55
+
56
+ export function searchTextFieldsChanged(input: Partial<TrapInput>): boolean {
57
+ return SEARCH_TEXT_FIELD_NAMES.some((field) => input[field] !== undefined);
58
+ }
59
+
60
+ export function passageFieldsChanged(input: TrapUpdate): boolean {
61
+ return PASSAGE_FIELD_NAMES.some((field) => input[field] !== undefined);
62
+ }
63
+
64
+ export function embeddingIsFresh(trap: Trap, embedding: StoredEmbedding | null, config: EmbeddingConfig): boolean {
65
+ return Boolean(
66
+ embedding &&
67
+ embedding.provider === config.provider &&
68
+ embedding.model === config.model &&
69
+ embedding.dimensions === config.dimensions &&
70
+ embedding.passage_version === config.passageVersion &&
71
+ embedding.passage_hash === passageHashForTrap(trap)
72
+ );
73
+ }
@@ -0,0 +1,26 @@
1
+ export const resourceDefinitions = [
2
+ {
3
+ uri: "codetrap://project/recent",
4
+ name: "Project Recent Traps",
5
+ description: "The 10 most recently updated traps in the current project",
6
+ mimeType: "application/json",
7
+ },
8
+ {
9
+ uri: "codetrap://global/recent",
10
+ name: "Global Recent Traps",
11
+ description: "The 10 most recently updated traps in the global database",
12
+ mimeType: "application/json",
13
+ },
14
+ {
15
+ uri: "codetrap://project/top",
16
+ name: "Project Top Traps",
17
+ description: "The 20 most frequently hit traps in the current project",
18
+ mimeType: "application/json",
19
+ },
20
+ {
21
+ uri: "codetrap://global/top",
22
+ name: "Global Top Traps",
23
+ description: "The 20 most frequently hit traps in the global database",
24
+ mimeType: "application/json",
25
+ },
26
+ ];