@stackwright-pro/otters 1.0.0-alpha.7 â 1.0.0-alpha.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.
- package/LICENSE +21 -0
- package/README.md +7 -6
- package/package.json +9 -7
- package/scripts/add-pipeline-declarations.py +201 -0
- package/scripts/generate-checksums.js +0 -1
- package/scripts/install-agents.js +5 -2
- package/scripts/launch-raft.cjs +13 -0
- package/scripts/sanity-check-pipeline.mjs +185 -0
- package/scripts/strip-artifact-schemas.cjs +133 -0
- package/src/checksums.json +15 -9
- package/src/stackwright-pro-api-otter.json +69 -95
- package/src/stackwright-pro-auth-otter.json +39 -790
- package/src/stackwright-pro-dashboard-otter.json +40 -662
- package/src/stackwright-pro-data-otter.json +36 -535
- package/src/stackwright-pro-designer-otter.json +17 -15
- package/src/stackwright-pro-domain-expert-otter.json +22 -0
- package/src/stackwright-pro-foreman-otter.json +29 -653
- package/src/stackwright-pro-form-wizard-otter.json +40 -0
- package/src/stackwright-pro-geo-otter.json +53 -0
- package/src/stackwright-pro-page-otter.json +27 -6
- package/src/stackwright-pro-polish-otter.json +62 -0
- package/src/stackwright-pro-qa-otter.json +63 -0
- package/src/stackwright-pro-scaffold-otter.json +40 -0
- package/src/stackwright-pro-theme-otter.json +34 -18
- package/src/stackwright-services-otter-system.md +192 -0
- package/src/stackwright-services-otter.json +61 -0
- package/src/python-bridge.ts +0 -391
- package/src/question-adapter.ts +0 -296
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
PROPRIETARY SOFTWARE LICENSE
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-2026 Per Aspera LLC. All Rights Reserved.
|
|
4
|
+
|
|
5
|
+
This software and associated documentation files (the "Software") are the
|
|
6
|
+
proprietary and confidential property of Per Aspera LLC ("Company").
|
|
7
|
+
|
|
8
|
+
RESTRICTIONS: You may not use, copy, modify, merge, publish, distribute,
|
|
9
|
+
sublicense, sell, or otherwise exploit this Software or any portion thereof
|
|
10
|
+
without the express prior written consent of the Company.
|
|
11
|
+
|
|
12
|
+
GOVERNMENT USE: Use, duplication, or disclosure by the U.S. Government is
|
|
13
|
+
subject to restrictions as set forth in FAR 52.227-19 (Commercial Computer
|
|
14
|
+
Software - Restricted Rights) and DFARS 252.227-7013 (Rights in Technical
|
|
15
|
+
Data and Computer Software), as applicable.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED. IN NO EVENT SHALL THE COMPANY BE LIABLE FOR ANY CLAIM, DAMAGES, OR
|
|
19
|
+
OTHER LIABILITY ARISING FROM THE USE OF THE SOFTWARE.
|
|
20
|
+
|
|
21
|
+
For licensing inquiries: legal@peraspera.com
|
package/README.md
CHANGED
|
@@ -118,12 +118,13 @@ When Pro otters combine with the OSS raft, these capabilities emerge:
|
|
|
118
118
|
|
|
119
119
|
## The Pro Otter Raft
|
|
120
120
|
|
|
121
|
-
| Otter
|
|
122
|
-
|
|
|
123
|
-
| đĻĻđĻĻ **Foreman Otter**
|
|
124
|
-
| đĻĻđĄ **API Otter**
|
|
125
|
-
| đĻĻđ **Dashboard Otter**
|
|
126
|
-
| đĻĻđ **Data Otter**
|
|
121
|
+
| Otter | Role | Output |
|
|
122
|
+
| ------------------------------------- | -------------------- | ------------------------------------------------------------------------------- |
|
|
123
|
+
| đĻĻđĻĻ **Foreman Otter** | Project coordinator | Orchestrates full-stack builds (delegates to unified Foreman) |
|
|
124
|
+
| đĻĻđĄ **API Otter** | OpenAPI discovery | API entity types, endpoints |
|
|
125
|
+
| đĻĻđ **Dashboard Otter** | Live data views | Typed API components |
|
|
126
|
+
| đĻĻđ **Data Otter** | Endpoint integration | ISR config, filters |
|
|
127
|
+
| `stackwright-pro-domain-expert-otter` | Utility | Answers specialist questions from the use case document in non-interactive mode |
|
|
127
128
|
|
|
128
129
|
---
|
|
129
130
|
|
package/package.json
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stackwright-pro/otters",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.70",
|
|
4
4
|
"description": "Stackwright Pro Otter Raft - AI agents for enterprise features (CAC auth, API dashboards, government use cases)",
|
|
5
|
-
"license": "
|
|
5
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "https://github.com/Per-Aspera-LLC/stackwright-pro"
|
|
9
9
|
},
|
|
10
10
|
"devDependencies": {
|
|
11
|
-
"vitest": "^4.
|
|
12
|
-
"zod": "^
|
|
11
|
+
"vitest": "^4.1.8",
|
|
12
|
+
"zod": "^4.4.3"
|
|
13
13
|
},
|
|
14
14
|
"exports": {
|
|
15
15
|
"./src": "./src",
|
|
16
|
-
"./pro-foreman": "./src/stackwright-pro-foreman-otter.json"
|
|
16
|
+
"./pro-foreman": "./src/stackwright-pro-foreman-otter.json",
|
|
17
|
+
"./pro-workflow": "./src/stackwright-pro-form-wizard-otter.json"
|
|
17
18
|
},
|
|
18
19
|
"files": [
|
|
19
20
|
"scripts",
|
|
@@ -23,7 +24,7 @@
|
|
|
23
24
|
"access": "public"
|
|
24
25
|
},
|
|
25
26
|
"peerDependencies": {
|
|
26
|
-
"@stackwright-pro/mcp": "^0.2.0-alpha.
|
|
27
|
+
"@stackwright-pro/mcp": "^0.2.0-alpha.103"
|
|
27
28
|
},
|
|
28
29
|
"scripts": {
|
|
29
30
|
"generate-checksums": "node scripts/generate-checksums.js",
|
|
@@ -31,6 +32,7 @@
|
|
|
31
32
|
"postinstall": "node scripts/install-agents.js",
|
|
32
33
|
"test": "vitest run",
|
|
33
34
|
"test:watch": "vitest",
|
|
34
|
-
"test:coverage": "vitest run --coverage"
|
|
35
|
+
"test:coverage": "vitest run --coverage",
|
|
36
|
+
"sanity-check-pipeline": "node scripts/sanity-check-pipeline.mjs"
|
|
35
37
|
}
|
|
36
38
|
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
add-pipeline-declarations.py
|
|
4
|
+
Inserts the `pipeline` I/O declaration into each specialist otter JSON.
|
|
5
|
+
|
|
6
|
+
Run once: python3 scripts/add-pipeline-declarations.py
|
|
7
|
+
Part of swp-amyw Batch 1.
|
|
8
|
+
|
|
9
|
+
AUDIT NOTES (inline justifications for dep changes):
|
|
10
|
+
- auth-otter: was ['pages', 'dashboard', 'workflow', 'geo'] in PHASE_DEPENDENCIES.
|
|
11
|
+
Bead 3 (swp-e4y6) changed the runtime model: AuthProvider reads _auth.json at
|
|
12
|
+
runtime, no longer needs build-time route enumeration. Auth only needs
|
|
13
|
+
design-language.json to extract personas/roles. Moves auth wave 4 â wave 2.
|
|
14
|
+
|
|
15
|
+
- workflow (form-wizard-otter): was [] with a Prism mock fallback comment.
|
|
16
|
+
Bead swp-4071 (services hooks contract) made the workflowâapi relationship
|
|
17
|
+
explicit via serviceHooks referencing API endpoints. Real deps added:
|
|
18
|
+
_theme.json (UI generation) and api-config.json (hook endpoint references).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
|
|
24
|
+
SRC_DIR = os.path.join(os.path.dirname(__file__), '..', 'src')
|
|
25
|
+
|
|
26
|
+
# Pipeline declarations keyed by otter filename (no .json extension).
|
|
27
|
+
# Matches the table in the swp-amyw plan.
|
|
28
|
+
# Null-sentinel otters (foreman, domain-expert) are skipped â they're orchestrators.
|
|
29
|
+
DECLARATIONS: dict[str, dict] = {
|
|
30
|
+
'stackwright-pro-designer-otter': {
|
|
31
|
+
'inputs': {},
|
|
32
|
+
'outputs': {
|
|
33
|
+
'artifact': 'design-language.json',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
'stackwright-pro-theme-otter': {
|
|
37
|
+
'inputs': {
|
|
38
|
+
'artifacts': ['design-language.json'],
|
|
39
|
+
},
|
|
40
|
+
'outputs': {
|
|
41
|
+
'sinks': ['_theme.json'],
|
|
42
|
+
'artifact': 'theme-tokens.json',
|
|
43
|
+
'files': ['stackwright.theme.yml'],
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
'stackwright-pro-api-otter': {
|
|
47
|
+
'inputs': {},
|
|
48
|
+
'outputs': {
|
|
49
|
+
'sinks': ['_integrations.json'],
|
|
50
|
+
'artifact': 'api-config.json',
|
|
51
|
+
'files': ['stackwright.integrations.yml'],
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
# AUDIT: auth moved from wave 4 â wave 2.
|
|
55
|
+
# Bead 3 (swp-e4y6) changed the runtime model: AuthProvider reads
|
|
56
|
+
# _auth.json at runtime; auth no longer needs build-time route enumeration.
|
|
57
|
+
# Auth now only reads design-language.json to extract personas/roles.
|
|
58
|
+
'stackwright-pro-auth-otter': {
|
|
59
|
+
'inputs': {
|
|
60
|
+
'artifacts': ['design-language.json'],
|
|
61
|
+
},
|
|
62
|
+
'outputs': {
|
|
63
|
+
'sinks': ['_auth.json'],
|
|
64
|
+
'artifact': 'auth-config.json',
|
|
65
|
+
'files': [
|
|
66
|
+
'stackwright.auth.yml',
|
|
67
|
+
'.env.example',
|
|
68
|
+
'lib/mock-auth.ts',
|
|
69
|
+
'package.json',
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
'stackwright-pro-scaffold-otter': {
|
|
74
|
+
'inputs': {
|
|
75
|
+
'sinks': ['_theme.json'],
|
|
76
|
+
'files': ['stackwright.yml'],
|
|
77
|
+
},
|
|
78
|
+
'outputs': {
|
|
79
|
+
'artifact': 'scaffold-manifest.json',
|
|
80
|
+
'files': ['app/**'],
|
|
81
|
+
},
|
|
82
|
+
# scaffold-otter generates app/layout.tsx which reads _collections.json
|
|
83
|
+
# and _auth.json at app build time â NOT at scaffold-otter execution time.
|
|
84
|
+
'runtimeReads': ['_collections.json', '_auth.json'],
|
|
85
|
+
},
|
|
86
|
+
'stackwright-pro-data-otter': {
|
|
87
|
+
'inputs': {
|
|
88
|
+
'artifacts': ['api-config.json'],
|
|
89
|
+
},
|
|
90
|
+
'outputs': {
|
|
91
|
+
'sinks': ['_collections.json'],
|
|
92
|
+
'artifact': 'data-config.json',
|
|
93
|
+
'files': ['stackwright.collections.yml'],
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
'stackwright-pro-geo-otter': {
|
|
97
|
+
'inputs': {
|
|
98
|
+
'artifacts': ['data-config.json'],
|
|
99
|
+
},
|
|
100
|
+
'outputs': {
|
|
101
|
+
'artifact': 'geo-manifest.json',
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
# AUDIT: workflow moved from [] â real deps.
|
|
105
|
+
# swp-4071 (services hooks contract) made workflowâapi relationship explicit:
|
|
106
|
+
# serviceHooks reference API endpoints from api-config.json.
|
|
107
|
+
# UI generation requires _theme.json for component selection.
|
|
108
|
+
'stackwright-pro-form-wizard-otter': {
|
|
109
|
+
'inputs': {
|
|
110
|
+
'sinks': ['_theme.json'],
|
|
111
|
+
'artifacts': ['api-config.json'],
|
|
112
|
+
},
|
|
113
|
+
'outputs': {
|
|
114
|
+
'artifact': 'workflow-config.json',
|
|
115
|
+
'files': ['stackwright.workflow.yml'],
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
'stackwright-services-otter': {
|
|
119
|
+
'inputs': {
|
|
120
|
+
'artifacts': ['api-config.json', 'workflow-config.json'],
|
|
121
|
+
},
|
|
122
|
+
'outputs': {
|
|
123
|
+
'artifact': 'services-config.json',
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
'stackwright-pro-page-otter': {
|
|
127
|
+
'inputs': {
|
|
128
|
+
'sinks': ['_theme.json', '_collections.json', '_auth.json'],
|
|
129
|
+
'artifacts': [
|
|
130
|
+
'design-language.json',
|
|
131
|
+
'data-config.json',
|
|
132
|
+
'services-config.json',
|
|
133
|
+
'geo-manifest.json',
|
|
134
|
+
],
|
|
135
|
+
},
|
|
136
|
+
'outputs': {
|
|
137
|
+
'artifact': 'pages-manifest.json',
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
'stackwright-pro-dashboard-otter': {
|
|
141
|
+
'inputs': {
|
|
142
|
+
'sinks': ['_theme.json', '_collections.json', '_auth.json'],
|
|
143
|
+
'artifacts': [
|
|
144
|
+
'design-language.json',
|
|
145
|
+
'data-config.json',
|
|
146
|
+
'services-config.json',
|
|
147
|
+
'geo-manifest.json',
|
|
148
|
+
],
|
|
149
|
+
},
|
|
150
|
+
'outputs': {
|
|
151
|
+
'artifact': 'dashboard-manifest.json',
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
'stackwright-pro-polish-otter': {
|
|
155
|
+
'inputs': {
|
|
156
|
+
'artifacts': [
|
|
157
|
+
'pages-manifest.json',
|
|
158
|
+
'dashboard-manifest.json',
|
|
159
|
+
'workflow-config.json',
|
|
160
|
+
'auth-config.json',
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
'outputs': {
|
|
164
|
+
'artifact': 'polish-manifest.json',
|
|
165
|
+
'files': ['stackwright.yml'],
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def add_pipeline(filename_stem: str, declaration: dict) -> None:
|
|
172
|
+
path = os.path.join(SRC_DIR, f'{filename_stem}.json')
|
|
173
|
+
if not os.path.exists(path):
|
|
174
|
+
print(f' SKIP (not found): {filename_stem}.json')
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
with open(path, 'r', encoding='utf-8') as fh:
|
|
178
|
+
data = json.load(fh)
|
|
179
|
+
|
|
180
|
+
if 'pipeline' in data:
|
|
181
|
+
print(f' â ALREADY HAS pipeline: {filename_stem}.json â overwriting')
|
|
182
|
+
|
|
183
|
+
data['pipeline'] = declaration
|
|
184
|
+
|
|
185
|
+
serialized = json.dumps(data, indent=2, ensure_ascii=False) + '\n'
|
|
186
|
+
with open(path, 'w', encoding='utf-8') as fh:
|
|
187
|
+
fh.write(serialized)
|
|
188
|
+
|
|
189
|
+
print(f' {filename_stem}.json')
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def main() -> None:
|
|
193
|
+
print('Adding pipeline declarations to specialist otter manifests...\n')
|
|
194
|
+
for stem, decl in DECLARATIONS.items():
|
|
195
|
+
add_pipeline(stem, decl)
|
|
196
|
+
|
|
197
|
+
print('\nDone. Run `node scripts/generate-checksums.js` to update checksums.json.')
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
if __name__ == '__main__':
|
|
201
|
+
main()
|
|
@@ -8,14 +8,17 @@ const fs = require('fs');
|
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const os = require('os');
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
// Allow test overrides via env vars â production paths are the default.
|
|
12
|
+
// STACKWRIGHT_AGENTS_DIR: override destination (normally ~/.code_puppy/agents/)
|
|
13
|
+
// STACKWRIGHT_OTTERS_SRC_DIR: override source (normally <package>/src/)
|
|
14
|
+
const AGENTS_DIR = process.env.STACKWRIGHT_AGENTS_DIR || path.join(os.homedir(), '.code_puppy', 'agents');
|
|
12
15
|
|
|
13
16
|
// Resolve package root relative to this script's location
|
|
14
17
|
const scriptDir = __dirname;
|
|
15
18
|
const packageRoot = path.resolve(scriptDir, '..');
|
|
16
19
|
|
|
17
20
|
// All Pro otters are in the src/ directory
|
|
18
|
-
const srcDir = path.join(packageRoot, 'src');
|
|
21
|
+
const srcDir = process.env.STACKWRIGHT_OTTERS_SRC_DIR || path.join(packageRoot, 'src');
|
|
19
22
|
|
|
20
23
|
async function installAgents() {
|
|
21
24
|
try {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// â ī¸ DEPRECATED â This launcher has been replaced by @stackwright-pro/raft.
|
|
4
|
+
// Run: npx @stackwright-pro/raft
|
|
5
|
+
// This file is kept for users who may have cached a previous install.
|
|
6
|
+
// It will be removed in a future release.
|
|
7
|
+
|
|
8
|
+
console.error('â ī¸ launch-raft in @stackwright-pro/otters is deprecated.');
|
|
9
|
+
console.error(' Use: npx @stackwright-pro/raft');
|
|
10
|
+
console.error('');
|
|
11
|
+
console.error(' Install: npm install -g @stackwright-pro/raft');
|
|
12
|
+
console.error(' Or run directly: npx @stackwright-pro/raft');
|
|
13
|
+
process.exit(1);
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* sanity-check-pipeline.mjs
|
|
4
|
+
* Verifies pipeline I/O declarations across all specialist otter manifests:
|
|
5
|
+
* 1. Every specialist otter (except foreman + domain-expert) has a pipeline field
|
|
6
|
+
* 2. No dangling sink references (every inputs.sinks value exists in some otter's outputs.sinks)
|
|
7
|
+
* 3. No dangling artifact references (every inputs.artifacts value exists in some otter's outputs.artifact)
|
|
8
|
+
* 4. No cycles (topological sort succeeds)
|
|
9
|
+
*
|
|
10
|
+
* Part of swp-amyw Batch 1 quality gate.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { readFileSync, readdirSync } from 'fs';
|
|
14
|
+
import { join, dirname } from 'path';
|
|
15
|
+
import { fileURLToPath } from 'url';
|
|
16
|
+
|
|
17
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const SRC_DIR = join(__dirname, '..', 'src');
|
|
19
|
+
|
|
20
|
+
// Orchestrators: exempt from pipeline requirement
|
|
21
|
+
const ORCHESTRATORS = new Set([
|
|
22
|
+
'stackwright-pro-foreman-otter',
|
|
23
|
+
'stackwright-pro-domain-expert-otter',
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
// âââ Load all otter manifests ââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
27
|
+
|
|
28
|
+
const otterFiles = readdirSync(SRC_DIR).filter(
|
|
29
|
+
(f) => f.endsWith('-otter.json') && !f.startsWith('checksums')
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const otters = otterFiles.map((filename) => {
|
|
33
|
+
const raw = readFileSync(join(SRC_DIR, filename), 'utf-8');
|
|
34
|
+
const data = JSON.parse(raw);
|
|
35
|
+
return { filename, name: data.name, pipeline: data.pipeline ?? null };
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
let errors = 0;
|
|
39
|
+
|
|
40
|
+
// âââ Check 1: Every specialist has a pipeline field âââââââââââââââââââââââââ
|
|
41
|
+
|
|
42
|
+
console.log('\n[1] Pipeline field presence...');
|
|
43
|
+
for (const { filename, name, pipeline } of otters) {
|
|
44
|
+
if (ORCHESTRATORS.has(name)) {
|
|
45
|
+
console.log(` SKIP (orchestrator): ${filename}`);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (pipeline === null || pipeline === undefined) {
|
|
49
|
+
console.error(` FAIL: missing pipeline in ${filename}`);
|
|
50
|
+
errors++;
|
|
51
|
+
} else {
|
|
52
|
+
console.log(` OK : ${filename}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// âââ Collect all declared sinks + artifacts ââââââââââââââââââââââââââââââââââ
|
|
57
|
+
|
|
58
|
+
const allOutputSinks = new Set();
|
|
59
|
+
const allOutputArtifacts = new Set();
|
|
60
|
+
|
|
61
|
+
for (const { pipeline } of otters) {
|
|
62
|
+
if (!pipeline) continue;
|
|
63
|
+
(pipeline.outputs?.sinks ?? []).forEach((s) => allOutputSinks.add(s));
|
|
64
|
+
if (pipeline.outputs?.artifact) allOutputArtifacts.add(pipeline.outputs.artifact);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// âââ Check 2: No dangling sink refs âââââââââââââââââââââââââââââââââââââââââ
|
|
68
|
+
|
|
69
|
+
console.log('\n[2] Dangling sink references...');
|
|
70
|
+
for (const { filename, pipeline } of otters) {
|
|
71
|
+
if (!pipeline) continue;
|
|
72
|
+
for (const sink of pipeline.inputs?.sinks ?? []) {
|
|
73
|
+
if (!allOutputSinks.has(sink)) {
|
|
74
|
+
console.error(` FAIL: ${filename} inputs.sinks references '${sink}' but no otter outputs it`);
|
|
75
|
+
errors++;
|
|
76
|
+
} else {
|
|
77
|
+
console.log(` OK : ${filename} <- ${sink}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (errors === 0) console.log(' (no dangling sinks)');
|
|
82
|
+
|
|
83
|
+
// âââ Check 3: No dangling artifact refs âââââââââââââââââââââââââââââââââââââ
|
|
84
|
+
|
|
85
|
+
console.log('\n[3] Dangling artifact references...');
|
|
86
|
+
let artifactRefs = 0;
|
|
87
|
+
for (const { filename, pipeline } of otters) {
|
|
88
|
+
if (!pipeline) continue;
|
|
89
|
+
for (const art of pipeline.inputs?.artifacts ?? []) {
|
|
90
|
+
artifactRefs++;
|
|
91
|
+
if (!allOutputArtifacts.has(art)) {
|
|
92
|
+
console.error(` FAIL: ${filename} inputs.artifacts references '${art}' but no otter outputs it`);
|
|
93
|
+
errors++;
|
|
94
|
+
} else {
|
|
95
|
+
console.log(` OK : ${filename} <- ${art}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (artifactRefs === 0) console.log(' (no artifact refs to check)');
|
|
100
|
+
|
|
101
|
+
// âââ Check 4: No cycles (Kahn's algorithm topological sort) âââââââââââââââââ
|
|
102
|
+
|
|
103
|
+
console.log('\n[4] Cycle detection (Kahn topological sort)...');
|
|
104
|
+
|
|
105
|
+
// Build adjacency by artifact: producer -> [consumers]
|
|
106
|
+
// Node = otter name
|
|
107
|
+
const otterByOutputArtifact = new Map();
|
|
108
|
+
for (const { name, pipeline } of otters) {
|
|
109
|
+
if (!pipeline) continue;
|
|
110
|
+
if (pipeline.outputs?.artifact) {
|
|
111
|
+
otterByOutputArtifact.set(pipeline.outputs.artifact, name);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Build adjacency list: name -> Set<name> (direct dependencies)
|
|
116
|
+
const deps = new Map();
|
|
117
|
+
for (const { name, pipeline } of otters) {
|
|
118
|
+
if (!pipeline || ORCHESTRATORS.has(name)) continue;
|
|
119
|
+
deps.set(name, new Set());
|
|
120
|
+
for (const art of pipeline.inputs?.artifacts ?? []) {
|
|
121
|
+
const producer = otterByOutputArtifact.get(art);
|
|
122
|
+
if (producer && producer !== name) deps.get(name).add(producer);
|
|
123
|
+
}
|
|
124
|
+
for (const sink of pipeline.inputs?.sinks ?? []) {
|
|
125
|
+
// find which otter outputs this sink
|
|
126
|
+
for (const { name: pName, pipeline: pPipeline } of otters) {
|
|
127
|
+
if (!pPipeline || pName === name) continue;
|
|
128
|
+
if ((pPipeline.outputs?.sinks ?? []).includes(sink)) {
|
|
129
|
+
deps.get(name).add(pName);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Kahn's: compute in-degree, enqueue zero-in-degree nodes
|
|
136
|
+
const inDegree = new Map();
|
|
137
|
+
const adjOut = new Map(); // name -> [names that depend on it]
|
|
138
|
+
for (const name of deps.keys()) {
|
|
139
|
+
inDegree.set(name, 0);
|
|
140
|
+
adjOut.set(name, []);
|
|
141
|
+
}
|
|
142
|
+
for (const [name, upstream] of deps.entries()) {
|
|
143
|
+
for (const u of upstream) {
|
|
144
|
+
inDegree.set(name, (inDegree.get(name) ?? 0) + 1);
|
|
145
|
+
if (!adjOut.has(u)) adjOut.set(u, []);
|
|
146
|
+
adjOut.get(u).push(name);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const queue = [];
|
|
151
|
+
for (const [name, deg] of inDegree.entries()) {
|
|
152
|
+
if (deg === 0) queue.push(name);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const sorted = [];
|
|
156
|
+
while (queue.length > 0) {
|
|
157
|
+
const node = queue.shift();
|
|
158
|
+
sorted.push(node);
|
|
159
|
+
for (const dependent of adjOut.get(node) ?? []) {
|
|
160
|
+
const newDeg = (inDegree.get(dependent) ?? 1) - 1;
|
|
161
|
+
inDegree.set(dependent, newDeg);
|
|
162
|
+
if (newDeg === 0) queue.push(dependent);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (sorted.length !== deps.size) {
|
|
167
|
+
console.error(` FAIL: cycle detected! Sorted ${sorted.length} of ${deps.size} nodes.`);
|
|
168
|
+
const unsorted = [...deps.keys()].filter((n) => !sorted.includes(n));
|
|
169
|
+
console.error(` Nodes in cycle: ${unsorted.join(', ')}`);
|
|
170
|
+
errors++;
|
|
171
|
+
} else {
|
|
172
|
+
console.log(` OK : topological order (${sorted.length} nodes):`);
|
|
173
|
+
console.log(` ${sorted.join(' -> ')}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// âââ Result ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
177
|
+
|
|
178
|
+
console.log('');
|
|
179
|
+
if (errors > 0) {
|
|
180
|
+
console.error(`SANITY CHECK FAILED â ${errors} error(s). Fix above before committing.`);
|
|
181
|
+
process.exit(1);
|
|
182
|
+
} else {
|
|
183
|
+
console.log('SANITY CHECK PASSED â no dangling refs, no cycles.');
|
|
184
|
+
process.exit(0);
|
|
185
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* strip-artifact-schemas.cjs
|
|
4
|
+
*
|
|
5
|
+
* Removes hardcoded artifact schema JSON blocks from specialist otter system_prompts.
|
|
6
|
+
* These are now injected dynamically via build_specialist_prompt (REQUIRED_ARTIFACT_SCHEMA).
|
|
7
|
+
*
|
|
8
|
+
* Run: node packages/otters/scripts/strip-artifact-schemas.cjs
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
const OTTERS_DIR = path.join(__dirname, '..', 'src');
|
|
15
|
+
const REFERENCE_LINE =
|
|
16
|
+
'**Artifact shape:** See the **REQUIRED_ARTIFACT_SCHEMA** section in your prompt for the canonical artifact shape. Use it when calling `stackwright_pro_validate_artifact`.';
|
|
17
|
+
|
|
18
|
+
function stripArtifactSchemaFromArray(promptArray) {
|
|
19
|
+
// Strategy: find the index of the ```json opening fence,
|
|
20
|
+
// then find the matching closing ``` fence.
|
|
21
|
+
// Replace everything from the fence through the closing fence + surrounding context
|
|
22
|
+
// with the REFERENCE_LINE.
|
|
23
|
+
|
|
24
|
+
const result = [];
|
|
25
|
+
let i = 0;
|
|
26
|
+
let stripped = false;
|
|
27
|
+
|
|
28
|
+
while (i < promptArray.length) {
|
|
29
|
+
const item = promptArray[i];
|
|
30
|
+
|
|
31
|
+
// Detect start of a JSON code block fence
|
|
32
|
+
if (typeof item === 'string' && item.trim() === '```json') {
|
|
33
|
+
// Find the closing fence
|
|
34
|
+
let j = i + 1;
|
|
35
|
+
while (j < promptArray.length && promptArray[j].trim() !== '```') {
|
|
36
|
+
j++;
|
|
37
|
+
}
|
|
38
|
+
// j now points to the closing ``` (or end of array)
|
|
39
|
+
|
|
40
|
+
// Replace this entire block with the reference line
|
|
41
|
+
// (but only the first JSON block per otter â the artifact schema block)
|
|
42
|
+
if (!stripped) {
|
|
43
|
+
result.push(REFERENCE_LINE);
|
|
44
|
+
stripped = true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Skip past the closing fence
|
|
48
|
+
i = j + 1;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
result.push(item);
|
|
53
|
+
i++;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function stripArtifactSchemaFromString(str) {
|
|
60
|
+
// For cases where the schema is embedded in a longer string as a ```json...``` block
|
|
61
|
+
// Replace the first ```json ... ``` block with the reference line
|
|
62
|
+
const jsonFenceRegex = /```json[\s\S]*?```/;
|
|
63
|
+
if (jsonFenceRegex.test(str)) {
|
|
64
|
+
return str.replace(jsonFenceRegex, REFERENCE_LINE);
|
|
65
|
+
}
|
|
66
|
+
return str;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function processOtterFile(filePath) {
|
|
70
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
71
|
+
const otter = JSON.parse(raw);
|
|
72
|
+
|
|
73
|
+
if (!Array.isArray(otter.system_prompt)) {
|
|
74
|
+
console.log(` â ī¸ ${path.basename(filePath)}: system_prompt is not an array â skipping`);
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Check if the artifact schema block is spread across array elements (```json as its own element)
|
|
79
|
+
// or embedded within a longer string
|
|
80
|
+
const hasFenceAsElement = otter.system_prompt.some(
|
|
81
|
+
(s) => typeof s === 'string' && s.trim() === '```json'
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const hasEmbeddedFence = otter.system_prompt.some(
|
|
85
|
+
(s) => typeof s === 'string' && s.includes('```json')
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
let modified = false;
|
|
89
|
+
|
|
90
|
+
if (hasFenceAsElement) {
|
|
91
|
+
// Multi-element block â strip using array strategy
|
|
92
|
+
const original = JSON.stringify(otter.system_prompt);
|
|
93
|
+
otter.system_prompt = stripArtifactSchemaFromArray(otter.system_prompt);
|
|
94
|
+
if (JSON.stringify(otter.system_prompt) !== original) {
|
|
95
|
+
modified = true;
|
|
96
|
+
}
|
|
97
|
+
} else if (hasEmbeddedFence) {
|
|
98
|
+
// Embedded fence within a string â strip using string replacement
|
|
99
|
+
const original = JSON.stringify(otter.system_prompt);
|
|
100
|
+
otter.system_prompt = otter.system_prompt.map((s) =>
|
|
101
|
+
typeof s === 'string' ? stripArtifactSchemaFromString(s) : s
|
|
102
|
+
);
|
|
103
|
+
if (JSON.stringify(otter.system_prompt) !== original) {
|
|
104
|
+
modified = true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (modified) {
|
|
109
|
+
fs.writeFileSync(filePath, JSON.stringify(otter, null, 2) + '\n');
|
|
110
|
+
console.log(` â
${path.basename(filePath)}: artifact schema block removed`);
|
|
111
|
+
} else {
|
|
112
|
+
console.log(
|
|
113
|
+
` âšī¸ ${path.basename(filePath)}: no artifact schema block found (may already be clean)`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return modified;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
console.log('Stripping hardcoded artifact schema blocks from specialist otters...\n');
|
|
121
|
+
|
|
122
|
+
const otterFiles = fs
|
|
123
|
+
.readdirSync(OTTERS_DIR)
|
|
124
|
+
.filter((f) => f.endsWith('-otter.json') && f !== 'stackwright-pro-foreman-otter.json')
|
|
125
|
+
.map((f) => path.join(OTTERS_DIR, f));
|
|
126
|
+
|
|
127
|
+
let totalModified = 0;
|
|
128
|
+
for (const filePath of otterFiles) {
|
|
129
|
+
const modified = processOtterFile(filePath);
|
|
130
|
+
if (modified) totalModified++;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
console.log(`\nDone. ${totalModified} otter file(s) modified.`);
|