@aryacode/dsh-engineering 0.8.4 → 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 +27 -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/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/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
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* @module dsh-engineering-l5x/session
|
|
13
13
|
*/
|
|
14
14
|
import { type L5xDocument } from './model/index.ts';
|
|
15
|
+
import { type L5xStyleProfile } from './engine/style.ts';
|
|
15
16
|
import type { ExecContext } from './core/tool.ts';
|
|
16
17
|
import { L5xError } from './core/errors.ts';
|
|
17
18
|
import type { FsService, FsTarget } from './core/tool.ts';
|
|
@@ -93,6 +94,14 @@ export declare class ProjectSession {
|
|
|
93
94
|
* @returns The open project.
|
|
94
95
|
*/
|
|
95
96
|
require(projectId: string | undefined, exec?: ExecContext): Promise<ProjectSnapshot>;
|
|
97
|
+
/**
|
|
98
|
+
* Read the house style of one open project.
|
|
99
|
+
*
|
|
100
|
+
* @param projectId - Explicit handle, or undefined for the chat's only project.
|
|
101
|
+
* @param exec - Calling context.
|
|
102
|
+
* @returns The profile in the shared resources schema.
|
|
103
|
+
*/
|
|
104
|
+
styleProfile(projectId: string | undefined, exec?: ExecContext): Promise<L5xStyleProfile>;
|
|
96
105
|
/**
|
|
97
106
|
* Resolve one path through the filesystem provider without reading it.
|
|
98
107
|
*
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { createHash, randomUUID } from 'node:crypto';
|
|
15
15
|
import { L5xParseError, loadL5x } from "./model/index.js";
|
|
16
|
+
import { styleProfileOf } from "./engine/style.js";
|
|
16
17
|
import { L5xError } from "./core/errors.js";
|
|
17
18
|
import { decodeExport } from "./fs.js";
|
|
18
19
|
/** A failure raised by an analysis engine. */
|
|
@@ -130,6 +131,16 @@ export class ProjectSession {
|
|
|
130
131
|
}
|
|
131
132
|
return project;
|
|
132
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Read the house style of one open project.
|
|
136
|
+
*
|
|
137
|
+
* @param projectId - Explicit handle, or undefined for the chat's only project.
|
|
138
|
+
* @param exec - Calling context.
|
|
139
|
+
* @returns The profile in the shared resources schema.
|
|
140
|
+
*/
|
|
141
|
+
async styleProfile(projectId, exec) {
|
|
142
|
+
return styleProfileOf(await this.require(projectId, exec));
|
|
143
|
+
}
|
|
133
144
|
/**
|
|
134
145
|
* Resolve one path through the filesystem provider without reading it.
|
|
135
146
|
*
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Engineering standards
|
|
2
|
+
|
|
3
|
+
Use this skill whenever you write, change or review control code or HMI screens — before you author, not
|
|
4
|
+
after.
|
|
5
|
+
|
|
6
|
+
## Read first
|
|
7
|
+
|
|
8
|
+
- `engineering_reference` with no arguments lists the corpus. Search it for the language and the domain
|
|
9
|
+
you are about to touch (`iec state machine`, `rockwell naming`, `view alarm`, `craft fault`), then read
|
|
10
|
+
the page. Each page carries rule IDs; cite the ID when you report a finding.
|
|
11
|
+
- When a project or export is open, call `engineering_profile` **before** writing. It reports the naming,
|
|
12
|
+
idioms and visual conventions the plant already uses, with evidence and a confidence level. Follow the
|
|
13
|
+
profile where it is confident; where it is low, say so and ask.
|
|
14
|
+
|
|
15
|
+
## Order of work
|
|
16
|
+
|
|
17
|
+
1. Read the profile (existing project) or the standard pages (new work).
|
|
18
|
+
2. Author the change with the project's own names and visual language.
|
|
19
|
+
3. `engineering_review` on the draft. Fix findings you agree with; explain the ones you do not.
|
|
20
|
+
4. Then run the product's own validation (`draft_validate`, `ftview_validate`, `mw_compile`).
|
|
21
|
+
|
|
22
|
+
## Honesty rules
|
|
23
|
+
|
|
24
|
+
- A profile is evidence, not authority: quote the counts and the source path when you rely on it.
|
|
25
|
+
- A rule ID is this bundle's opinion. Never present it as a standards clause or a certification.
|
|
26
|
+
- Do not reproduce standards text; link the page instead.
|
|
27
|
+
- If the corpus or a profile is unavailable, say which one and continue with the product's own checks.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loading and searching the engineering corpus.
|
|
3
|
+
*
|
|
4
|
+
* The corpus is Markdown with front matter; `scripts/gen-resources.mjs` turns it
|
|
5
|
+
* into `index.json` and `SOURCE.json`. This module reads only those generated
|
|
6
|
+
* files plus the page bodies, so searching is deterministic and needs no model,
|
|
7
|
+
* network or tokenizer.
|
|
8
|
+
*
|
|
9
|
+
* @module dsh-engineering-resources/corpus
|
|
10
|
+
*/
|
|
11
|
+
/** One page as the index records it. */
|
|
12
|
+
export interface PageRow {
|
|
13
|
+
readonly id: string;
|
|
14
|
+
readonly title: string;
|
|
15
|
+
readonly domain: string;
|
|
16
|
+
readonly level: string;
|
|
17
|
+
readonly basis: readonly string[];
|
|
18
|
+
readonly tags: readonly string[];
|
|
19
|
+
readonly applies_to: readonly string[];
|
|
20
|
+
readonly rules: readonly string[];
|
|
21
|
+
readonly words: number;
|
|
22
|
+
readonly bytes: number;
|
|
23
|
+
readonly sha256: string;
|
|
24
|
+
}
|
|
25
|
+
/** The generated index. */
|
|
26
|
+
export interface CorpusIndex {
|
|
27
|
+
readonly corpus: number;
|
|
28
|
+
readonly pages: readonly PageRow[];
|
|
29
|
+
}
|
|
30
|
+
/** One hit from a search. */
|
|
31
|
+
export interface CorpusHit {
|
|
32
|
+
readonly id: string;
|
|
33
|
+
readonly title: string;
|
|
34
|
+
readonly domain: string;
|
|
35
|
+
readonly level: string;
|
|
36
|
+
readonly rules: readonly string[];
|
|
37
|
+
readonly score: number;
|
|
38
|
+
}
|
|
39
|
+
/** The corpus in memory, or the reason it is unavailable. */
|
|
40
|
+
export interface Corpus {
|
|
41
|
+
readonly index: CorpusIndex;
|
|
42
|
+
readonly digest: string;
|
|
43
|
+
readonly directory: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Load the index and source pin once per process.
|
|
47
|
+
*
|
|
48
|
+
* @returns The corpus.
|
|
49
|
+
* @throws when the generated files are missing, which means the build did not run.
|
|
50
|
+
*/
|
|
51
|
+
export declare function corpus(): Corpus;
|
|
52
|
+
/**
|
|
53
|
+
* Read one page body.
|
|
54
|
+
*
|
|
55
|
+
* @param id - Page id, as listed by the contents.
|
|
56
|
+
* @returns Markdown body.
|
|
57
|
+
*/
|
|
58
|
+
export declare function pageBody(id: string): string;
|
|
59
|
+
/**
|
|
60
|
+
* Rank pages by metadata, then by body text for the remainder.
|
|
61
|
+
*
|
|
62
|
+
* @param query - Free text, rule id, tag or page id.
|
|
63
|
+
* @param limit - Most hits to return.
|
|
64
|
+
* @param domain - Optional domain filter.
|
|
65
|
+
* @returns Ranked hits, best first.
|
|
66
|
+
*/
|
|
67
|
+
export declare function search(query: string, limit?: number, domain?: string): readonly CorpusHit[];
|
|
68
|
+
/** Domain order for the contents listing. */
|
|
69
|
+
export declare const DOMAINS: readonly ["iec", "rockwell", "yaskawa", "view", "optix", "craft"];
|
|
70
|
+
/** The contents, grouped by domain, with word counts. */
|
|
71
|
+
export declare function contents(): {
|
|
72
|
+
domains: readonly string[];
|
|
73
|
+
pages: readonly {
|
|
74
|
+
id: string;
|
|
75
|
+
title: string;
|
|
76
|
+
domain: string;
|
|
77
|
+
level: string;
|
|
78
|
+
words: number;
|
|
79
|
+
}[];
|
|
80
|
+
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loading and searching the engineering corpus.
|
|
3
|
+
*
|
|
4
|
+
* The corpus is Markdown with front matter; `scripts/gen-resources.mjs` turns it
|
|
5
|
+
* into `index.json` and `SOURCE.json`. This module reads only those generated
|
|
6
|
+
* files plus the page bodies, so searching is deterministic and needs no model,
|
|
7
|
+
* network or tokenizer.
|
|
8
|
+
*
|
|
9
|
+
* @module dsh-engineering-resources/corpus
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
12
|
+
import { dirname, join } from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
/** Where the corpus lives, relative to the built module. */
|
|
15
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
16
|
+
let cached;
|
|
17
|
+
/**
|
|
18
|
+
* Load the index and source pin once per process.
|
|
19
|
+
*
|
|
20
|
+
* @returns The corpus.
|
|
21
|
+
* @throws when the generated files are missing, which means the build did not run.
|
|
22
|
+
*/
|
|
23
|
+
export function corpus() {
|
|
24
|
+
if (cached !== undefined)
|
|
25
|
+
return cached;
|
|
26
|
+
const indexPath = join(ROOT, 'index.json');
|
|
27
|
+
const sourcePath = join(ROOT, 'SOURCE.json');
|
|
28
|
+
if (!existsSync(indexPath) || !existsSync(sourcePath)) {
|
|
29
|
+
throw Object.assign(new Error('The engineering corpus is missing; run `pnpm resources:build` before packing.'), { code: 'RESOURCES_MISSING' });
|
|
30
|
+
}
|
|
31
|
+
const index = JSON.parse(readFileSync(indexPath, 'utf8'));
|
|
32
|
+
const source = JSON.parse(readFileSync(sourcePath, 'utf8'));
|
|
33
|
+
if (!Array.isArray(index.pages) || index.pages.length === 0) {
|
|
34
|
+
throw Object.assign(new Error('The engineering corpus is empty; run `pnpm resources:build`.'), { code: 'RESOURCES_MISSING' });
|
|
35
|
+
}
|
|
36
|
+
cached = { index, digest: source.digest, directory: ROOT };
|
|
37
|
+
return cached;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Read one page body.
|
|
41
|
+
*
|
|
42
|
+
* @param id - Page id, as listed by the contents.
|
|
43
|
+
* @returns Markdown body.
|
|
44
|
+
*/
|
|
45
|
+
export function pageBody(id) {
|
|
46
|
+
const { index } = corpus();
|
|
47
|
+
const row = index.pages.find(page => page.id === id);
|
|
48
|
+
if (row === undefined) {
|
|
49
|
+
throw Object.assign(new Error(`No resource page named "${id}".`), { code: 'COMPONENT_NOT_FOUND' });
|
|
50
|
+
}
|
|
51
|
+
const path = join(ROOT, 'corpus', row.domain, `${row.id}.md`);
|
|
52
|
+
if (!existsSync(path)) {
|
|
53
|
+
throw Object.assign(new Error(`The page file for "${id}" is missing from the corpus.`), { code: 'RESOURCES_MISSING' });
|
|
54
|
+
}
|
|
55
|
+
// Strip the front matter: the index already carries it, and repeating it in
|
|
56
|
+
// the answer spends tokens for nothing.
|
|
57
|
+
return readFileSync(path, 'utf8').replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/u, '');
|
|
58
|
+
}
|
|
59
|
+
/** Score one page against a query. Higher is better; zero is no hit. */
|
|
60
|
+
function scoreOf(row, needle) {
|
|
61
|
+
if (row.id === needle)
|
|
62
|
+
return 100;
|
|
63
|
+
if (row.rules.some(rule => rule.toLowerCase() === needle))
|
|
64
|
+
return 95;
|
|
65
|
+
if (row.title.toLowerCase() === needle)
|
|
66
|
+
return 90;
|
|
67
|
+
if (row.tags.some(tag => tag.toLowerCase() === needle))
|
|
68
|
+
return 80;
|
|
69
|
+
if (row.id.includes(needle))
|
|
70
|
+
return 60;
|
|
71
|
+
if (row.title.toLowerCase().includes(needle))
|
|
72
|
+
return 55;
|
|
73
|
+
if (row.tags.some(tag => tag.toLowerCase().includes(needle)))
|
|
74
|
+
return 45;
|
|
75
|
+
if (row.rules.some(rule => rule.toLowerCase().startsWith(needle)))
|
|
76
|
+
return 40;
|
|
77
|
+
if (row.domain === needle)
|
|
78
|
+
return 35;
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Rank pages by metadata, then by body text for the remainder.
|
|
83
|
+
*
|
|
84
|
+
* @param query - Free text, rule id, tag or page id.
|
|
85
|
+
* @param limit - Most hits to return.
|
|
86
|
+
* @param domain - Optional domain filter.
|
|
87
|
+
* @returns Ranked hits, best first.
|
|
88
|
+
*/
|
|
89
|
+
export function search(query, limit = 8, domain) {
|
|
90
|
+
const { index } = corpus();
|
|
91
|
+
const needle = query.trim().toLowerCase();
|
|
92
|
+
const tokens = needle.split(/\s+/u).filter(Boolean);
|
|
93
|
+
const pool = domain === undefined ? index.pages : index.pages.filter(page => page.domain === domain);
|
|
94
|
+
const scored = pool
|
|
95
|
+
.map(row => ({ row, score: tokens.reduce((sum, token) => sum + scoreOf(row, token), 0) }))
|
|
96
|
+
.filter(hit => hit.score > 0);
|
|
97
|
+
if (scored.length < limit) {
|
|
98
|
+
const seen = new Set(scored.map(hit => hit.row.id));
|
|
99
|
+
for (const row of pool) {
|
|
100
|
+
if (seen.has(row.id))
|
|
101
|
+
continue;
|
|
102
|
+
try {
|
|
103
|
+
const body = pageBody(row.id).toLowerCase();
|
|
104
|
+
if (tokens.every(token => body.includes(token)))
|
|
105
|
+
scored.push({ row, score: 20 });
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// A missing page file is reported when it is read, not during ranking.
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return scored
|
|
113
|
+
.sort((left, right) => right.score - left.score || left.row.id.localeCompare(right.row.id))
|
|
114
|
+
.slice(0, Math.min(Math.max(limit, 1), 20))
|
|
115
|
+
.map(hit => ({ id: hit.row.id, title: hit.row.title, domain: hit.row.domain, level: hit.row.level, rules: hit.row.rules, score: hit.score }));
|
|
116
|
+
}
|
|
117
|
+
/** Domain order for the contents listing. */
|
|
118
|
+
export const DOMAINS = ['iec', 'rockwell', 'yaskawa', 'view', 'optix', 'craft'];
|
|
119
|
+
/** The contents, grouped by domain, with word counts. */
|
|
120
|
+
export function contents() {
|
|
121
|
+
const { index } = corpus();
|
|
122
|
+
return {
|
|
123
|
+
domains: DOMAINS.filter(domain => index.pages.some(page => page.domain === domain)),
|
|
124
|
+
pages: index.pages.map(page => ({ id: page.id, title: page.title, domain: page.domain, level: page.level, words: page.words })),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
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 type { StyleProfile } from './profile.ts';
|
|
14
|
+
/** Plugin name in a composition. */
|
|
15
|
+
export declare const name = "engineering-resources";
|
|
16
|
+
/** The tools need the registry; the skill is registered when the host offers it. */
|
|
17
|
+
export declare const inject: string[];
|
|
18
|
+
/** The service name a generation tool can inject to read a profile by handle. */
|
|
19
|
+
export declare const PROFILES_SERVICE = "engineeringProfiles";
|
|
20
|
+
/** The skill metadata, kept here so a test can assert it without the file. */
|
|
21
|
+
export declare const SKILL: {
|
|
22
|
+
name: string;
|
|
23
|
+
description: string;
|
|
24
|
+
whenToUse: string;
|
|
25
|
+
};
|
|
26
|
+
/** Configuration for the row. */
|
|
27
|
+
export interface Config {
|
|
28
|
+
/** Whether the reference and profile tools are registered. Defaults to true. */
|
|
29
|
+
reference?: boolean;
|
|
30
|
+
}
|
|
31
|
+
/** The service exposed to other rows. */
|
|
32
|
+
export interface ProfilesService {
|
|
33
|
+
/** The profile for one handle, when it is still in memory. */
|
|
34
|
+
get(id: string): StyleProfile | undefined;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Register the resource tools and the standards skill.
|
|
38
|
+
*
|
|
39
|
+
* @param ctx - Plugin context with the tool registry and optional services.
|
|
40
|
+
* @param config - Row configuration.
|
|
41
|
+
*/
|
|
42
|
+
export declare function apply(ctx: {
|
|
43
|
+
tools: {
|
|
44
|
+
register(definition: unknown): void;
|
|
45
|
+
};
|
|
46
|
+
get?: (name: string) => unknown;
|
|
47
|
+
provide?: (name: string, value: unknown) => void;
|
|
48
|
+
logger?: {
|
|
49
|
+
warn?(message: string): void;
|
|
50
|
+
};
|
|
51
|
+
}, config?: Config): void;
|
|
52
|
+
export { getProfile } from './store.ts';
|
|
53
|
+
export { validateProfile, profileId } from './profile.ts';
|
|
54
|
+
export type { StyleProfile } from './profile.ts';
|
|
55
|
+
export { resourceTools, referenceTool, profileTool } from './tools.ts';
|
|
@@ -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
|
+
}
|