@aryacode/dsh-engineering 0.8.3 → 0.9.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/CHANGELOG.md +43 -0
- package/README.md +7 -1
- package/cordis.patch.yml +7 -0
- package/entries/resources.js +23 -0
- package/package.json +6 -3
- package/vendor/ftview/SKILL.md +4 -1
- package/vendor/ftview/lib/edit/tools.d.ts +1 -1
- package/vendor/ftview/lib/edit/tools.js +26 -6
- package/vendor/ftview/lib/edit.js +2 -1
- package/vendor/ftview/lib/session.d.ts +9 -0
- package/vendor/ftview/lib/session.js +11 -0
- package/vendor/ftview/lib/style.d.ts +60 -0
- package/vendor/ftview/lib/style.js +109 -0
- package/vendor/ftview/lib/vendor-tools.js +5 -2
- package/vendor/ftview/lib/vendor.d.ts +43 -3
- package/vendor/ftview/lib/vendor.js +95 -5
- package/vendor/l5x/lib/engine/style.d.ts +67 -0
- package/vendor/l5x/lib/engine/style.js +119 -0
- package/vendor/l5x/lib/session.d.ts +9 -0
- package/vendor/l5x/lib/session.js +11 -0
- package/vendor/resources/SKILL.md +27 -0
- package/vendor/resources/lib/corpus.d.ts +80 -0
- package/vendor/resources/lib/corpus.js +126 -0
- package/vendor/resources/lib/index.d.ts +55 -0
- package/vendor/resources/lib/index.js +75 -0
- package/vendor/resources/lib/profile.d.ts +113 -0
- package/vendor/resources/lib/profile.js +52 -0
- package/vendor/resources/lib/store.d.ts +22 -0
- package/vendor/resources/lib/store.js +35 -0
- package/vendor/resources/lib/tools.d.ts +138 -0
- package/vendor/resources/lib/tools.js +190 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The engineering resources row: a local standards corpus, a house-style profile
|
|
3
|
+
* and the `engineering-standards` skill that tells a model to read both before
|
|
4
|
+
* authoring.
|
|
5
|
+
*
|
|
6
|
+
* The corpus is generated data (`index.json`, `SOURCE.json`) over authored
|
|
7
|
+
* Markdown; the profile is produced by whichever product package has a project
|
|
8
|
+
* open. This entry only wires them to the tool registry and, when the host offers
|
|
9
|
+
* it, to the skills service.
|
|
10
|
+
*
|
|
11
|
+
* @module dsh-engineering-resources
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
14
|
+
import { dirname, join } from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import { resourceTools } from "./tools.js";
|
|
17
|
+
import { getProfile } from "./store.js";
|
|
18
|
+
/** Plugin name in a composition. */
|
|
19
|
+
export const name = 'engineering-resources';
|
|
20
|
+
/** The tools need the registry; the skill is registered when the host offers it. */
|
|
21
|
+
export const inject = ['tools'];
|
|
22
|
+
/** The service name a generation tool can inject to read a profile by handle. */
|
|
23
|
+
export const PROFILES_SERVICE = 'engineeringProfiles';
|
|
24
|
+
/** The skill metadata, kept here so a test can assert it without the file. */
|
|
25
|
+
export const SKILL = {
|
|
26
|
+
name: 'engineering-standards',
|
|
27
|
+
description: 'Engineering standards and house style for control code and HMI: read the local corpus for IEC 61131-3, Rockwell, Yaskawa, FactoryTalk and craft rules, profile the open project to follow its own conventions, and cite rule IDs in findings.',
|
|
28
|
+
whenToUse: 'Before writing or reviewing PLC code, ladder, Structured Text, FactoryTalk View or Optix screens — especially when matching an existing project’s style, or when starting new work that should meet a professional standard.',
|
|
29
|
+
};
|
|
30
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
31
|
+
/** The skill file, in the built package or the source tree. */
|
|
32
|
+
function skillPath() {
|
|
33
|
+
for (const candidate of [join(HERE, '..', 'SKILL.md'), join(HERE, '..', '..', 'SKILL.md')]) {
|
|
34
|
+
if (existsSync(candidate))
|
|
35
|
+
return candidate;
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Register the resource tools and the standards skill.
|
|
41
|
+
*
|
|
42
|
+
* @param ctx - Plugin context with the tool registry and optional services.
|
|
43
|
+
* @param config - Row configuration.
|
|
44
|
+
*/
|
|
45
|
+
export function apply(ctx, config = {}) {
|
|
46
|
+
const getService = (service) => (typeof ctx.get === 'function' ? ctx.get(service) : undefined);
|
|
47
|
+
if (config.reference !== false) {
|
|
48
|
+
for (const definition of resourceTools(getService))
|
|
49
|
+
ctx.tools.register(definition);
|
|
50
|
+
}
|
|
51
|
+
ctx.provide?.(PROFILES_SERVICE, { get: getProfile });
|
|
52
|
+
const path = skillPath();
|
|
53
|
+
const skills = getService('skills');
|
|
54
|
+
if (path === undefined || skills?.register === undefined) {
|
|
55
|
+
try {
|
|
56
|
+
ctx.logger?.warn?.('engineering-resources: the engineering-standards skill was not registered; the tools are available.');
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// A logger that refuses must not turn a missing skill into a crash.
|
|
60
|
+
}
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const content = readFileSync(path, 'utf8').replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/u, '');
|
|
64
|
+
skills.register({
|
|
65
|
+
...SKILL,
|
|
66
|
+
content,
|
|
67
|
+
path,
|
|
68
|
+
source: 'custom',
|
|
69
|
+
provider: '@aryacode/dsh-engineering',
|
|
70
|
+
invocation: { modelInvocable: true, userInvocable: true },
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
export { getProfile } from "./store.js";
|
|
74
|
+
export { validateProfile, profileId } from "./profile.js";
|
|
75
|
+
export { resourceTools, referenceTool, profileTool } from "./tools.js";
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The house-style profile: the evidence-backed conventions of one open project.
|
|
3
|
+
*
|
|
4
|
+
* A profile is produced by a product package (it owns the parser) and validated
|
|
5
|
+
* here. It is deliberately small JSON: every field names its evidence and the
|
|
6
|
+
* whole profile carries a confidence level, because a convention inferred from
|
|
7
|
+
* six rungs is a hint, not a rule.
|
|
8
|
+
*
|
|
9
|
+
* @module dsh-engineering-resources/profile
|
|
10
|
+
*/
|
|
11
|
+
/** One counted convention, e.g. a tag prefix. */
|
|
12
|
+
export interface Counted {
|
|
13
|
+
/** The observed value. */
|
|
14
|
+
readonly value: string;
|
|
15
|
+
/** How many times it was observed. */
|
|
16
|
+
readonly count: number;
|
|
17
|
+
}
|
|
18
|
+
/** Evidence behind one claim. */
|
|
19
|
+
export interface ProfileEvidence {
|
|
20
|
+
/** File the evidence came from. */
|
|
21
|
+
readonly file?: string;
|
|
22
|
+
/** Rung or line number, when the product can name one. */
|
|
23
|
+
readonly line?: number;
|
|
24
|
+
/** How many observations support the claim. */
|
|
25
|
+
readonly count?: number;
|
|
26
|
+
/** Human-readable note. */
|
|
27
|
+
readonly note: string;
|
|
28
|
+
}
|
|
29
|
+
/** Kind of project the profile was read from. */
|
|
30
|
+
export type ProfileKind = 'l5x' | 'ftview' | 'mwi' | 'optix';
|
|
31
|
+
/** The profile schema every product returns. */
|
|
32
|
+
export interface StyleProfile {
|
|
33
|
+
/** Schema version; refuse anything else. */
|
|
34
|
+
readonly schema: 1;
|
|
35
|
+
/** Where the profile came from. */
|
|
36
|
+
readonly source: {
|
|
37
|
+
readonly kind: ProfileKind;
|
|
38
|
+
readonly path: string;
|
|
39
|
+
readonly sha256?: string;
|
|
40
|
+
};
|
|
41
|
+
/** Naming conventions, when the product can infer them. */
|
|
42
|
+
readonly naming?: {
|
|
43
|
+
readonly tags?: {
|
|
44
|
+
readonly prefixes: readonly Counted[];
|
|
45
|
+
readonly case?: string;
|
|
46
|
+
};
|
|
47
|
+
readonly routines?: {
|
|
48
|
+
readonly case?: string;
|
|
49
|
+
readonly examples?: readonly string[];
|
|
50
|
+
};
|
|
51
|
+
readonly displays?: {
|
|
52
|
+
readonly case?: string;
|
|
53
|
+
readonly examples?: readonly string[];
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
/** Logic idioms the project actually uses. */
|
|
57
|
+
readonly idioms?: {
|
|
58
|
+
readonly sealIn?: {
|
|
59
|
+
readonly count: number;
|
|
60
|
+
readonly form: string;
|
|
61
|
+
};
|
|
62
|
+
readonly latch?: {
|
|
63
|
+
readonly count: number;
|
|
64
|
+
};
|
|
65
|
+
readonly stateMachine?: {
|
|
66
|
+
readonly count: number;
|
|
67
|
+
readonly form: string;
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
/** HMI conventions, for FactoryTalk View and Optix. */
|
|
71
|
+
readonly hmi?: {
|
|
72
|
+
readonly palette?: readonly Counted[];
|
|
73
|
+
readonly fonts?: readonly Counted[];
|
|
74
|
+
readonly button?: readonly Counted[];
|
|
75
|
+
readonly navigation?: {
|
|
76
|
+
readonly displaysWithNav: number;
|
|
77
|
+
readonly displays: number;
|
|
78
|
+
};
|
|
79
|
+
readonly popups?: {
|
|
80
|
+
readonly onTop: number;
|
|
81
|
+
readonly replace: number;
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
/** Comment density, where the product exposes comments. */
|
|
85
|
+
readonly comments?: {
|
|
86
|
+
readonly total: number;
|
|
87
|
+
readonly commented: number;
|
|
88
|
+
readonly ratio: number;
|
|
89
|
+
};
|
|
90
|
+
/** What the profile is based on. */
|
|
91
|
+
readonly evidence: readonly ProfileEvidence[];
|
|
92
|
+
/** How much to trust it. */
|
|
93
|
+
readonly confidence: 'low' | 'medium' | 'high';
|
|
94
|
+
}
|
|
95
|
+
/** A validation result: the profile, or the reasons it was refused. */
|
|
96
|
+
export interface ProfileCheck {
|
|
97
|
+
readonly ok: boolean;
|
|
98
|
+
readonly problems: readonly string[];
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Validate one profile from a product package.
|
|
102
|
+
*
|
|
103
|
+
* @param value - Whatever the product returned.
|
|
104
|
+
* @returns Ok, or the specific problems.
|
|
105
|
+
*/
|
|
106
|
+
export declare function validateProfile(value: unknown): ProfileCheck;
|
|
107
|
+
/**
|
|
108
|
+
* A stable handle for one profile, so a tool can refer to it later.
|
|
109
|
+
*
|
|
110
|
+
* @param profile - A validated profile.
|
|
111
|
+
* @returns Short id derived from the source and its digest.
|
|
112
|
+
*/
|
|
113
|
+
export declare function profileId(profile: StyleProfile): string;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The house-style profile: the evidence-backed conventions of one open project.
|
|
3
|
+
*
|
|
4
|
+
* A profile is produced by a product package (it owns the parser) and validated
|
|
5
|
+
* here. It is deliberately small JSON: every field names its evidence and the
|
|
6
|
+
* whole profile carries a confidence level, because a convention inferred from
|
|
7
|
+
* six rungs is a hint, not a rule.
|
|
8
|
+
*
|
|
9
|
+
* @module dsh-engineering-resources/profile
|
|
10
|
+
*/
|
|
11
|
+
import { createHash } from 'node:crypto';
|
|
12
|
+
/**
|
|
13
|
+
* Validate one profile from a product package.
|
|
14
|
+
*
|
|
15
|
+
* @param value - Whatever the product returned.
|
|
16
|
+
* @returns Ok, or the specific problems.
|
|
17
|
+
*/
|
|
18
|
+
export function validateProfile(value) {
|
|
19
|
+
const problems = [];
|
|
20
|
+
if (typeof value !== 'object' || value === null)
|
|
21
|
+
return { ok: false, problems: ['profile is not an object'] };
|
|
22
|
+
const profile = value;
|
|
23
|
+
if (profile.schema !== 1)
|
|
24
|
+
problems.push('schema must be 1');
|
|
25
|
+
const source = profile.source;
|
|
26
|
+
if (source === undefined)
|
|
27
|
+
problems.push('source is required');
|
|
28
|
+
else {
|
|
29
|
+
if (!['l5x', 'ftview', 'mwi', 'optix'].includes(source.kind))
|
|
30
|
+
problems.push(`unknown source.kind ${JSON.stringify(source.kind)}`);
|
|
31
|
+
if (typeof source.path !== 'string' || source.path === '')
|
|
32
|
+
problems.push('source.path is required');
|
|
33
|
+
}
|
|
34
|
+
if (!['low', 'medium', 'high'].includes(profile.confidence ?? ''))
|
|
35
|
+
problems.push('confidence must be low, medium or high');
|
|
36
|
+
if (!Array.isArray(profile.evidence) || profile.evidence.length === 0)
|
|
37
|
+
problems.push('evidence must be a non-empty array');
|
|
38
|
+
if (profile.naming === undefined && profile.idioms === undefined && profile.hmi === undefined && profile.comments === undefined) {
|
|
39
|
+
problems.push('a profile needs at least one of naming, idioms, hmi or comments');
|
|
40
|
+
}
|
|
41
|
+
return { ok: problems.length === 0, problems };
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* A stable handle for one profile, so a tool can refer to it later.
|
|
45
|
+
*
|
|
46
|
+
* @param profile - A validated profile.
|
|
47
|
+
* @returns Short id derived from the source and its digest.
|
|
48
|
+
*/
|
|
49
|
+
export function profileId(profile) {
|
|
50
|
+
const seed = JSON.stringify([profile.source.kind, profile.source.path, profile.source.sha256 ?? '', profile.evidence.length]);
|
|
51
|
+
return createHash('sha256').update(seed).digest('hex').slice(0, 12);
|
|
52
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat-scoped storage for house-style profiles.
|
|
3
|
+
*
|
|
4
|
+
* A profile is produced by a product tool call and consumed by a generation tool
|
|
5
|
+
* later in the same conversation, so it lives in memory under a short handle. It
|
|
6
|
+
* is rebuilt from the project whenever the agent asks again; nothing is written
|
|
7
|
+
* to disk.
|
|
8
|
+
*
|
|
9
|
+
* @module dsh-engineering-resources/store
|
|
10
|
+
*/
|
|
11
|
+
import type { StyleProfile } from './profile.ts';
|
|
12
|
+
/**
|
|
13
|
+
* Remember one profile under its handle.
|
|
14
|
+
*
|
|
15
|
+
* @param id - Handle from `profileId`.
|
|
16
|
+
* @param profile - The validated profile.
|
|
17
|
+
*/
|
|
18
|
+
export declare function rememberProfile(id: string, profile: StyleProfile): void;
|
|
19
|
+
/** The profile for one handle, when it is still in memory. */
|
|
20
|
+
export declare function getProfile(id: string): StyleProfile | undefined;
|
|
21
|
+
/** Drop every profile; tests use this between cases. */
|
|
22
|
+
export declare function clearProfiles(): void;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat-scoped storage for house-style profiles.
|
|
3
|
+
*
|
|
4
|
+
* A profile is produced by a product tool call and consumed by a generation tool
|
|
5
|
+
* later in the same conversation, so it lives in memory under a short handle. It
|
|
6
|
+
* is rebuilt from the project whenever the agent asks again; nothing is written
|
|
7
|
+
* to disk.
|
|
8
|
+
*
|
|
9
|
+
* @module dsh-engineering-resources/store
|
|
10
|
+
*/
|
|
11
|
+
/** Most profiles one process keeps before dropping the oldest. */
|
|
12
|
+
const MAX = 32;
|
|
13
|
+
const profiles = new Map();
|
|
14
|
+
/**
|
|
15
|
+
* Remember one profile under its handle.
|
|
16
|
+
*
|
|
17
|
+
* @param id - Handle from `profileId`.
|
|
18
|
+
* @param profile - The validated profile.
|
|
19
|
+
*/
|
|
20
|
+
export function rememberProfile(id, profile) {
|
|
21
|
+
if (profiles.size >= MAX) {
|
|
22
|
+
const oldest = profiles.keys().next().value;
|
|
23
|
+
if (oldest !== undefined)
|
|
24
|
+
profiles.delete(oldest);
|
|
25
|
+
}
|
|
26
|
+
profiles.set(id, profile);
|
|
27
|
+
}
|
|
28
|
+
/** The profile for one handle, when it is still in memory. */
|
|
29
|
+
export function getProfile(id) {
|
|
30
|
+
return profiles.get(id);
|
|
31
|
+
}
|
|
32
|
+
/** Drop every profile; tests use this between cases. */
|
|
33
|
+
export function clearProfiles() {
|
|
34
|
+
profiles.clear();
|
|
35
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The three resource tools: read the corpus, profile the open project, review a
|
|
3
|
+
* draft.
|
|
4
|
+
*
|
|
5
|
+
* They are deliberately thin. The corpus tool reads generated files; the profile
|
|
6
|
+
* tool asks the product package that owns the parser; the review tool compares a
|
|
7
|
+
* product's prepared findings with the profile and the corpus rule IDs.
|
|
8
|
+
*
|
|
9
|
+
* @module dsh-engineering-resources/tools
|
|
10
|
+
*/
|
|
11
|
+
import { getProfile } from './store.ts';
|
|
12
|
+
/** The reference tool definition. */
|
|
13
|
+
export declare const referenceTool: {
|
|
14
|
+
name: string;
|
|
15
|
+
description: string;
|
|
16
|
+
parameters: {
|
|
17
|
+
type: string;
|
|
18
|
+
properties: {
|
|
19
|
+
query: {
|
|
20
|
+
type: string;
|
|
21
|
+
description: string;
|
|
22
|
+
};
|
|
23
|
+
id: {
|
|
24
|
+
type: string;
|
|
25
|
+
description: string;
|
|
26
|
+
};
|
|
27
|
+
domain: {
|
|
28
|
+
type: string;
|
|
29
|
+
description: string;
|
|
30
|
+
};
|
|
31
|
+
limit: {
|
|
32
|
+
type: string;
|
|
33
|
+
description: string;
|
|
34
|
+
minimum: number;
|
|
35
|
+
maximum: number;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
additionalProperties: boolean;
|
|
39
|
+
required: never[];
|
|
40
|
+
};
|
|
41
|
+
output: {
|
|
42
|
+
schema: {
|
|
43
|
+
type: string;
|
|
44
|
+
additionalProperties: boolean;
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
execute(args?: Readonly<Record<string, unknown>>): Promise<unknown>;
|
|
48
|
+
};
|
|
49
|
+
/** The profile tool, bound to the product services the composition provides. */
|
|
50
|
+
export declare function profileTool(getService: (name: string) => unknown): {
|
|
51
|
+
name: string;
|
|
52
|
+
description: string;
|
|
53
|
+
parameters: {
|
|
54
|
+
type: string;
|
|
55
|
+
properties: {
|
|
56
|
+
project_id: {
|
|
57
|
+
type: string;
|
|
58
|
+
description: string;
|
|
59
|
+
};
|
|
60
|
+
view_id: {
|
|
61
|
+
type: string;
|
|
62
|
+
description: string;
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
additionalProperties: boolean;
|
|
66
|
+
required: never[];
|
|
67
|
+
};
|
|
68
|
+
output: {
|
|
69
|
+
schema: {
|
|
70
|
+
type: string;
|
|
71
|
+
additionalProperties: boolean;
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
execute(args?: Readonly<Record<string, unknown>>, exec?: unknown): Promise<unknown>;
|
|
75
|
+
};
|
|
76
|
+
/** Every tool this package registers. */
|
|
77
|
+
export declare function resourceTools(getService: (name: string) => unknown): ({
|
|
78
|
+
name: string;
|
|
79
|
+
description: string;
|
|
80
|
+
parameters: {
|
|
81
|
+
type: string;
|
|
82
|
+
properties: {
|
|
83
|
+
query: {
|
|
84
|
+
type: string;
|
|
85
|
+
description: string;
|
|
86
|
+
};
|
|
87
|
+
id: {
|
|
88
|
+
type: string;
|
|
89
|
+
description: string;
|
|
90
|
+
};
|
|
91
|
+
domain: {
|
|
92
|
+
type: string;
|
|
93
|
+
description: string;
|
|
94
|
+
};
|
|
95
|
+
limit: {
|
|
96
|
+
type: string;
|
|
97
|
+
description: string;
|
|
98
|
+
minimum: number;
|
|
99
|
+
maximum: number;
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
additionalProperties: boolean;
|
|
103
|
+
required: never[];
|
|
104
|
+
};
|
|
105
|
+
output: {
|
|
106
|
+
schema: {
|
|
107
|
+
type: string;
|
|
108
|
+
additionalProperties: boolean;
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
execute(args?: Readonly<Record<string, unknown>>): Promise<unknown>;
|
|
112
|
+
} | {
|
|
113
|
+
name: string;
|
|
114
|
+
description: string;
|
|
115
|
+
parameters: {
|
|
116
|
+
type: string;
|
|
117
|
+
properties: {
|
|
118
|
+
project_id: {
|
|
119
|
+
type: string;
|
|
120
|
+
description: string;
|
|
121
|
+
};
|
|
122
|
+
view_id: {
|
|
123
|
+
type: string;
|
|
124
|
+
description: string;
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
additionalProperties: boolean;
|
|
128
|
+
required: never[];
|
|
129
|
+
};
|
|
130
|
+
output: {
|
|
131
|
+
schema: {
|
|
132
|
+
type: string;
|
|
133
|
+
additionalProperties: boolean;
|
|
134
|
+
};
|
|
135
|
+
};
|
|
136
|
+
execute(args?: Readonly<Record<string, unknown>>, exec?: unknown): Promise<unknown>;
|
|
137
|
+
})[];
|
|
138
|
+
export { getProfile };
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The three resource tools: read the corpus, profile the open project, review a
|
|
3
|
+
* draft.
|
|
4
|
+
*
|
|
5
|
+
* They are deliberately thin. The corpus tool reads generated files; the profile
|
|
6
|
+
* tool asks the product package that owns the parser; the review tool compares a
|
|
7
|
+
* product's prepared findings with the profile and the corpus rule IDs.
|
|
8
|
+
*
|
|
9
|
+
* @module dsh-engineering-resources/tools
|
|
10
|
+
*/
|
|
11
|
+
import { contents, pageBody, search, corpus } from "./corpus.js";
|
|
12
|
+
import { profileId, validateProfile } from "./profile.js";
|
|
13
|
+
import { getProfile, rememberProfile } from "./store.js";
|
|
14
|
+
/** The failure envelope every engineering tool returns. */
|
|
15
|
+
function failure(error) {
|
|
16
|
+
const code = typeof error === 'object' && error !== null && typeof error.code === 'string'
|
|
17
|
+
? error.code
|
|
18
|
+
: 'INTERNAL';
|
|
19
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
20
|
+
return {
|
|
21
|
+
error: {
|
|
22
|
+
code,
|
|
23
|
+
message,
|
|
24
|
+
fix: code === 'RESOURCES_MISSING'
|
|
25
|
+
? 'Run `pnpm resources:build` before packing; the corpus index is generated.'
|
|
26
|
+
: code === 'COMPONENT_NOT_FOUND'
|
|
27
|
+
? 'Call engineering_reference with no arguments for the available page ids and rule ids.'
|
|
28
|
+
: 'Correct the argument and retry; the corpus is local and needs no network.',
|
|
29
|
+
candidates: [],
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** Coverage carried by every reference answer. */
|
|
34
|
+
const REFERENCE_COVERAGE = {
|
|
35
|
+
complete: false,
|
|
36
|
+
unverified: [
|
|
37
|
+
'whether the advice fits this machine; a profile is evidence, not authority',
|
|
38
|
+
'the normative text of the standards the pages are based on',
|
|
39
|
+
],
|
|
40
|
+
};
|
|
41
|
+
/** Coverage carried by every profile answer. */
|
|
42
|
+
const PROFILE_COVERAGE = {
|
|
43
|
+
complete: false,
|
|
44
|
+
unverified: [
|
|
45
|
+
'runtime behaviour of the inferred conventions',
|
|
46
|
+
'conventions the parser cannot see (external HMI, undocumented practice)',
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
/** The reference tool definition. */
|
|
50
|
+
export const referenceTool = {
|
|
51
|
+
name: 'engineering_reference',
|
|
52
|
+
description: 'Read the local engineering corpus: return the contents with no arguments, ranked pages for a query, or one page in full for an id. Pages carry rule IDs (CRAFT-NAME-01, VIEW-HPM-02, …) and their basis; nothing is fetched from the network.',
|
|
53
|
+
parameters: {
|
|
54
|
+
type: 'object',
|
|
55
|
+
properties: {
|
|
56
|
+
query: { type: 'string', description: 'Topic, rule id or tag to search for, for example "state machine", "VIEW-HPM-02" or "seal-in".' },
|
|
57
|
+
id: { type: 'string', description: 'Exact page id from the contents, for example "iec-state-machines".' },
|
|
58
|
+
domain: { type: 'string', description: 'Optional domain filter: iec, rockwell, yaskawa, view, optix or craft.' },
|
|
59
|
+
limit: { type: 'integer', description: 'Most search hits to return; defaults to 8, maximum 20.', minimum: 1, maximum: 20 },
|
|
60
|
+
},
|
|
61
|
+
additionalProperties: false,
|
|
62
|
+
required: [],
|
|
63
|
+
},
|
|
64
|
+
output: { schema: { type: 'object', additionalProperties: true } },
|
|
65
|
+
async execute(args = {}) {
|
|
66
|
+
try {
|
|
67
|
+
const id = typeof args.id === 'string' && args.id.trim() !== '' ? args.id.trim() : undefined;
|
|
68
|
+
const loaded = corpus();
|
|
69
|
+
if (id !== undefined) {
|
|
70
|
+
const row = loaded.index.pages.find(page => page.id === id);
|
|
71
|
+
if (row === undefined)
|
|
72
|
+
throw Object.assign(new Error(`No resource page named "${id}".`), { code: 'COMPONENT_NOT_FOUND' });
|
|
73
|
+
const markdown = pageBody(id);
|
|
74
|
+
return {
|
|
75
|
+
summary: `Read ${row.title} (${row.domain}, ${row.level}) — rules ${row.rules.join(', ')}.`,
|
|
76
|
+
data: {
|
|
77
|
+
id,
|
|
78
|
+
title: row.title,
|
|
79
|
+
domain: row.domain,
|
|
80
|
+
level: row.level,
|
|
81
|
+
basis: row.basis,
|
|
82
|
+
rules: row.rules,
|
|
83
|
+
words: row.words,
|
|
84
|
+
corpus_digest: loaded.digest,
|
|
85
|
+
markdown,
|
|
86
|
+
},
|
|
87
|
+
next: [{ tool: 'engineering_profile', why: 'Read the open project’s conventions before writing.' }],
|
|
88
|
+
coverage: REFERENCE_COVERAGE,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const query = typeof args.query === 'string' && args.query.trim() !== '' ? args.query.trim() : undefined;
|
|
92
|
+
if (query === undefined) {
|
|
93
|
+
const listing = contents();
|
|
94
|
+
return {
|
|
95
|
+
summary: `${loaded.index.pages.length} engineering resource pages across ${listing.domains.length} domains.`,
|
|
96
|
+
data: { corpus_digest: loaded.digest, domains: listing.domains, pages: listing.pages },
|
|
97
|
+
next: [{ tool: 'engineering_reference', args: { query: 'state machine' }, why: 'Search for the topic you are about to touch.' }],
|
|
98
|
+
coverage: REFERENCE_COVERAGE,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const domain = typeof args.domain === 'string' && args.domain !== '' ? args.domain : undefined;
|
|
102
|
+
const requested = typeof args.limit === 'number' && Number.isInteger(args.limit) ? args.limit : 8;
|
|
103
|
+
const hits = search(query, requested, domain);
|
|
104
|
+
return {
|
|
105
|
+
summary: hits.length === 0 ? `No resource page matches "${query}".` : `${hits.length} resource page(s) match "${query}".`,
|
|
106
|
+
data: {
|
|
107
|
+
corpus_digest: loaded.digest,
|
|
108
|
+
query,
|
|
109
|
+
hits,
|
|
110
|
+
...(hits.length === 0 ? { suggestion: 'Call engineering_reference with no arguments for the contents.' } : {}),
|
|
111
|
+
},
|
|
112
|
+
next: hits.length === 0
|
|
113
|
+
? [{ tool: 'engineering_reference', why: 'List the contents and pick an id.' }]
|
|
114
|
+
: [{ tool: 'engineering_reference', args: { id: hits[0]?.id }, why: 'Read the best-matching page.' }],
|
|
115
|
+
coverage: REFERENCE_COVERAGE,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
return failure(error);
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
/** The profile tool, bound to the product services the composition provides. */
|
|
124
|
+
export function profileTool(getService) {
|
|
125
|
+
return {
|
|
126
|
+
name: 'engineering_profile',
|
|
127
|
+
description: 'Read the house style of the open project: naming, logic idioms, HMI palette and fonts, comment density, with the evidence and a confidence level. Call it before authoring so new code follows the plant’s own conventions.',
|
|
128
|
+
parameters: {
|
|
129
|
+
type: 'object',
|
|
130
|
+
properties: {
|
|
131
|
+
project_id: { type: 'string', description: 'L5X project handle from open_project. Omit when exactly one project is open.' },
|
|
132
|
+
view_id: { type: 'string', description: 'FactoryTalk View handle from ftview_open. Provide this or project_id.' },
|
|
133
|
+
},
|
|
134
|
+
additionalProperties: false,
|
|
135
|
+
required: [],
|
|
136
|
+
},
|
|
137
|
+
output: { schema: { type: 'object', additionalProperties: true } },
|
|
138
|
+
async execute(args = {}, exec) {
|
|
139
|
+
try {
|
|
140
|
+
const projectId = typeof args.project_id === 'string' && args.project_id !== '' ? args.project_id : undefined;
|
|
141
|
+
const viewId = typeof args.view_id === 'string' && args.view_id !== '' ? args.view_id : undefined;
|
|
142
|
+
const l5x = getService('l5xProjects');
|
|
143
|
+
const ftview = getService('ftviewViews');
|
|
144
|
+
const target = viewId !== undefined
|
|
145
|
+
? { service: ftview, id: viewId, name: 'FactoryTalk View' }
|
|
146
|
+
: { service: l5x, id: projectId, name: 'L5X' };
|
|
147
|
+
if (target.service?.styleProfile === undefined) {
|
|
148
|
+
return {
|
|
149
|
+
error: {
|
|
150
|
+
code: 'PROFILE_UNAVAILABLE',
|
|
151
|
+
message: `No ${target.name} project service is available in this composition, so a profile cannot be read.`,
|
|
152
|
+
fix: viewId !== undefined
|
|
153
|
+
? 'Open the export with ftview_open first, and make sure the ftview row is enabled.'
|
|
154
|
+
: 'Open the project with open_project first, and make sure the l5x row is enabled.',
|
|
155
|
+
candidates: [],
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const profile = await target.service.styleProfile(target.id, exec);
|
|
160
|
+
const check = validateProfile(profile);
|
|
161
|
+
if (!check.ok) {
|
|
162
|
+
return {
|
|
163
|
+
error: {
|
|
164
|
+
code: 'PROFILE_INVALID',
|
|
165
|
+
message: `The ${target.name} package returned a profile this version cannot read: ${check.problems.join('; ')}.`,
|
|
166
|
+
fix: 'Update the bundle; the profile schema is versioned.',
|
|
167
|
+
candidates: [],
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const id = profileId(profile);
|
|
172
|
+
rememberProfile(id, profile);
|
|
173
|
+
return {
|
|
174
|
+
summary: `${profile.source.kind} profile for ${profile.source.path}: confidence ${profile.confidence}, ${profile.evidence.length} evidence item(s).`,
|
|
175
|
+
data: { profile_id: id, profile },
|
|
176
|
+
next: [{ tool: 'engineering_reference', query: profile.source.kind === 'ftview' ? 'hmi' : 'state machine', why: 'Read the standard that applies to the change you are about to make.' }],
|
|
177
|
+
coverage: PROFILE_COVERAGE,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
return failure(error);
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
/** Every tool this package registers. */
|
|
187
|
+
export function resourceTools(getService) {
|
|
188
|
+
return [referenceTool, profileTool(getService)];
|
|
189
|
+
}
|
|
190
|
+
export { getProfile };
|