@starci/skills 1.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.
- package/INDEX.md +74 -0
- package/INDEX.vi.md +75 -0
- package/README.md +44 -0
- package/README.vi.md +43 -0
- package/SKILL.md +135 -0
- package/SKILL.vi.md +128 -0
- package/alias/INDEX.md +104 -0
- package/alias/INDEX.vi.md +104 -0
- package/alias/alias.json +297 -0
- package/bin/starci-skills.mjs +219 -0
- package/knowledge/INDEX.md +22 -0
- package/knowledge/INDEX.vi.md +22 -0
- package/knowledge/grammars/starci/DNA.md +146 -0
- package/knowledge/grammars/starci/DNA.vi.md +146 -0
- package/knowledge/grammars/starci/INDEX.md +25 -0
- package/knowledge/grammars/starci/INDEX.vi.md +25 -0
- package/knowledge/grammars/starci/family.md +50 -0
- package/knowledge/grammars/starci/family.vi.md +50 -0
- package/knowledge/grammars/starci/idioms.md +120 -0
- package/knowledge/grammars/starci/idioms.vi.md +121 -0
- package/knowledge/grammars/starci/playbook.md +36 -0
- package/knowledge/grammars/starci/playbook.vi.md +35 -0
- package/knowledge/patterns/be/INDEX.md +29 -0
- package/knowledge/patterns/be/INDEX.vi.md +29 -0
- package/knowledge/patterns/be/comment.md +80 -0
- package/knowledge/patterns/be/comment.vi.md +80 -0
- package/knowledge/patterns/be/error.md +86 -0
- package/knowledge/patterns/be/error.vi.md +86 -0
- package/knowledge/patterns/be/folder.md +87 -0
- package/knowledge/patterns/be/folder.vi.md +86 -0
- package/knowledge/patterns/be/function.md +80 -0
- package/knowledge/patterns/be/function.vi.md +80 -0
- package/knowledge/patterns/be/imports.md +79 -0
- package/knowledge/patterns/be/imports.vi.md +79 -0
- package/knowledge/patterns/be/naming.md +87 -0
- package/knowledge/patterns/be/naming.vi.md +87 -0
- package/knowledge/patterns/be/test.md +79 -0
- package/knowledge/patterns/be/test.vi.md +79 -0
- package/knowledge/patterns/be/typing.md +73 -0
- package/knowledge/patterns/be/typing.vi.md +73 -0
- package/knowledge/patterns/fe/INDEX.md +29 -0
- package/knowledge/patterns/fe/INDEX.vi.md +29 -0
- package/knowledge/patterns/fe/comment.md +70 -0
- package/knowledge/patterns/fe/comment.vi.md +70 -0
- package/knowledge/patterns/fe/error.md +61 -0
- package/knowledge/patterns/fe/error.vi.md +61 -0
- package/knowledge/patterns/fe/folder.md +98 -0
- package/knowledge/patterns/fe/folder.vi.md +95 -0
- package/knowledge/patterns/fe/function.md +70 -0
- package/knowledge/patterns/fe/function.vi.md +70 -0
- package/knowledge/patterns/fe/imports.md +87 -0
- package/knowledge/patterns/fe/imports.vi.md +87 -0
- package/knowledge/patterns/fe/naming.md +77 -0
- package/knowledge/patterns/fe/naming.vi.md +77 -0
- package/knowledge/patterns/fe/test.md +73 -0
- package/knowledge/patterns/fe/test.vi.md +73 -0
- package/knowledge/patterns/fe/typing.md +67 -0
- package/knowledge/patterns/fe/typing.vi.md +67 -0
- package/knowledge/ui/INDEX.md +101 -0
- package/knowledge/ui/INDEX.vi.md +101 -0
- package/knowledge/ui/composition/INDEX.md +60 -0
- package/knowledge/ui/composition/INDEX.vi.md +63 -0
- package/knowledge/ui/composition/accent.md +73 -0
- package/knowledge/ui/composition/accent.vi.md +74 -0
- package/knowledge/ui/composition/action.md +55 -0
- package/knowledge/ui/composition/action.vi.md +56 -0
- package/knowledge/ui/composition/coverage.md +37 -0
- package/knowledge/ui/composition/coverage.vi.md +37 -0
- package/knowledge/ui/composition/cta.md +79 -0
- package/knowledge/ui/composition/cta.vi.md +79 -0
- package/knowledge/ui/composition/feedback.md +55 -0
- package/knowledge/ui/composition/feedback.vi.md +56 -0
- package/knowledge/ui/composition/hierarchy.md +83 -0
- package/knowledge/ui/composition/hierarchy.vi.md +82 -0
- package/knowledge/ui/composition/layout.md +91 -0
- package/knowledge/ui/composition/layout.vi.md +93 -0
- package/knowledge/ui/composition/responsive.md +67 -0
- package/knowledge/ui/composition/responsive.vi.md +67 -0
- package/knowledge/ui/composition/state.md +105 -0
- package/knowledge/ui/composition/state.vi.md +106 -0
- package/knowledge/ui/presentation/INDEX.md +94 -0
- package/knowledge/ui/presentation/INDEX.vi.md +69 -0
- package/knowledge/ui/presentation/boundary.md +170 -0
- package/knowledge/ui/presentation/boundary.vi.md +169 -0
- package/knowledge/ui/presentation/font.md +155 -0
- package/knowledge/ui/presentation/font.vi.md +156 -0
- package/knowledge/ui/presentation/gap.md +181 -0
- package/knowledge/ui/presentation/gap.vi.md +181 -0
- package/knowledge/ui/presentation/margin.md +168 -0
- package/knowledge/ui/presentation/margin.vi.md +171 -0
- package/knowledge/ui/presentation/measure.md +174 -0
- package/knowledge/ui/presentation/measure.vi.md +178 -0
- package/knowledge/ui/presentation/overflow.md +142 -0
- package/knowledge/ui/presentation/overflow.vi.md +142 -0
- package/knowledge/ui/presentation/padding.md +293 -0
- package/knowledge/ui/presentation/padding.vi.md +292 -0
- package/knowledge/ui/presentation/surface.md +166 -0
- package/knowledge/ui/presentation/surface.vi.md +166 -0
- package/knowledge/ui/presentation/text-flow.md +123 -0
- package/knowledge/ui/presentation/text-flow.vi.md +123 -0
- package/knowledge/ui/presentation/tone.md +114 -0
- package/knowledge/ui/presentation/tone.vi.md +113 -0
- package/knowledge/ui/proof/INDEX.md +55 -0
- package/knowledge/ui/proof/INDEX.vi.md +55 -0
- package/knowledge/ui/proof/accessibility.md +67 -0
- package/knowledge/ui/proof/accessibility.vi.md +68 -0
- package/knowledge/ui/proof/contrast.md +58 -0
- package/knowledge/ui/proof/contrast.vi.md +57 -0
- package/knowledge/ui/proof/focus.md +76 -0
- package/knowledge/ui/proof/focus.vi.md +76 -0
- package/knowledge/ui/proof/motion.md +63 -0
- package/knowledge/ui/proof/motion.vi.md +64 -0
- package/knowledge/ui/proof/render-truth.md +63 -0
- package/knowledge/ui/proof/render-truth.vi.md +63 -0
- package/operators/INDEX.md +199 -0
- package/operators/INDEX.vi.md +199 -0
- package/operators/architecture-decide/errors.json +115 -0
- package/operators/architecture-decide/operator.json +20 -0
- package/operators/architecture-decide/operator.md +133 -0
- package/operators/architecture-decide/operator.vi.md +130 -0
- package/operators/architecture-decide/self-test.mjs +221 -0
- package/operators/architecture-decide/validate.mjs +112 -0
- package/operators/backend-source-apply/errors.json +66 -0
- package/operators/backend-source-apply/operator.json +20 -0
- package/operators/backend-source-apply/operator.md +189 -0
- package/operators/backend-source-apply/operator.vi.md +185 -0
- package/operators/backend-source-apply/self-test.mjs +257 -0
- package/operators/backend-source-apply/validate.mjs +240 -0
- package/operators/business-decide/errors.json +78 -0
- package/operators/business-decide/operator.json +20 -0
- package/operators/business-decide/operator.md +176 -0
- package/operators/business-decide/operator.vi.md +174 -0
- package/operators/business-decide/self-test.mjs +225 -0
- package/operators/business-decide/validate.mjs +277 -0
- package/operators/content-generate/errors.json +106 -0
- package/operators/content-generate/operator.json +21 -0
- package/operators/content-generate/operator.md +155 -0
- package/operators/content-generate/operator.vi.md +155 -0
- package/operators/content-generate/self-test.mjs +288 -0
- package/operators/content-generate/validate.mjs +196 -0
- package/operators/errors.json +178 -0
- package/operators/frontend-direction-decide/errors.json +135 -0
- package/operators/frontend-direction-decide/operator.json +21 -0
- package/operators/frontend-direction-decide/operator.md +167 -0
- package/operators/frontend-direction-decide/operator.vi.md +167 -0
- package/operators/frontend-direction-decide/self-test.mjs +226 -0
- package/operators/frontend-direction-decide/validate.mjs +146 -0
- package/operators/frontend-presentation-resolve/errors.json +42 -0
- package/operators/frontend-presentation-resolve/operator.json +19 -0
- package/operators/frontend-presentation-resolve/operator.md +142 -0
- package/operators/frontend-presentation-resolve/operator.vi.md +140 -0
- package/operators/frontend-presentation-resolve/self-test.mjs +163 -0
- package/operators/frontend-presentation-resolve/validate.mjs +143 -0
- package/operators/frontend-source-apply/errors.json +30 -0
- package/operators/frontend-source-apply/operator.json +20 -0
- package/operators/frontend-source-apply/operator.md +127 -0
- package/operators/frontend-source-apply/operator.vi.md +126 -0
- package/operators/frontend-source-apply/self-test.mjs +214 -0
- package/operators/frontend-source-apply/validate.mjs +133 -0
- package/operators/frontend-surface-audit/errors.json +5 -0
- package/operators/frontend-surface-audit/operator.json +22 -0
- package/operators/frontend-surface-audit/operator.md +121 -0
- package/operators/frontend-surface-audit/operator.vi.md +121 -0
- package/operators/frontend-surface-audit/self-test.mjs +185 -0
- package/operators/frontend-surface-audit/validate.mjs +127 -0
- package/operators/git-publish/errors.json +54 -0
- package/operators/git-publish/operator.json +20 -0
- package/operators/git-publish/operator.md +180 -0
- package/operators/git-publish/operator.vi.md +178 -0
- package/operators/git-publish/self-test.mjs +177 -0
- package/operators/git-publish/validate.mjs +106 -0
- package/operators/platform-operate/errors.json +90 -0
- package/operators/platform-operate/operator.json +22 -0
- package/operators/platform-operate/operator.md +162 -0
- package/operators/platform-operate/operator.vi.md +160 -0
- package/operators/platform-operate/self-test.mjs +202 -0
- package/operators/platform-operate/validate.mjs +193 -0
- package/operators/quality-verify/errors.json +54 -0
- package/operators/quality-verify/operator.json +20 -0
- package/operators/quality-verify/operator.md +185 -0
- package/operators/quality-verify/operator.vi.md +177 -0
- package/operators/quality-verify/self-test.mjs +210 -0
- package/operators/quality-verify/validate.mjs +197 -0
- package/operators/release-deploy/errors.json +158 -0
- package/operators/release-deploy/operator.json +23 -0
- package/operators/release-deploy/operator.md +183 -0
- package/operators/release-deploy/operator.vi.md +181 -0
- package/operators/release-deploy/self-test.mjs +235 -0
- package/operators/release-deploy/validate.mjs +129 -0
- package/operators/uat-verify/errors.json +78 -0
- package/operators/uat-verify/operator.json +25 -0
- package/operators/uat-verify/operator.md +162 -0
- package/operators/uat-verify/operator.vi.md +161 -0
- package/operators/uat-verify/self-test.mjs +270 -0
- package/operators/uat-verify/validate.mjs +202 -0
- package/operators/workspace-bind/errors.json +90 -0
- package/operators/workspace-bind/operator.json +21 -0
- package/operators/workspace-bind/operator.md +148 -0
- package/operators/workspace-bind/operator.vi.md +148 -0
- package/operators/workspace-bind/self-test.mjs +205 -0
- package/operators/workspace-bind/validate.mjs +152 -0
- package/package.json +50 -0
- package/readiness/initialization/workspaces/commit-policy.json +91 -0
- package/readiness/initialization/workspaces/config.schema.json +35 -0
- package/readiness/initialization/workspaces/device-state.schema.json +54 -0
- package/readiness/initialization/workspaces/local-route.schema.json +206 -0
- package/readiness/initialization/workspaces/portable-route.schema.json +200 -0
- package/resources/INDEX.md +96 -0
- package/resources/INDEX.vi.md +99 -0
- package/resources/agents/profiles/claude.json +131 -0
- package/resources/agents/profiles/openai.json +131 -0
- package/resources/orchestrator.json +71 -0
- package/resources/tools.json +85 -0
- package/routing.json +272 -0
- package/scripts/alias-registry.mjs +31 -0
- package/scripts/device-state.mjs +497 -0
- package/scripts/device-state.spec.mjs +18 -0
- package/scripts/errors-registry.mjs +68 -0
- package/scripts/generate-alias-doc.mjs +66 -0
- package/scripts/generate-grammar-dna.mjs +387 -0
- package/scripts/generate-operators-index.mjs +95 -0
- package/scripts/generate-presentation-owned.mjs +681 -0
- package/scripts/install-cli.spec.mjs +74 -0
- package/scripts/json-schema.mjs +94 -0
- package/scripts/operator-md.mjs +96 -0
- package/scripts/run-operator-self-tests.mjs +36 -0
- package/scripts/validate-alias.mjs +165 -0
- package/scripts/validate-defaults.mjs +72 -0
- package/scripts/validate-knowledge-citations.mjs +90 -0
- package/scripts/validate-operator.mjs +125 -0
- package/scripts/validate-request.mjs +80 -0
- package/scripts/validate-resources.mjs +117 -0
- package/scripts/validate-response.mjs +141 -0
- package/scripts/validate-routing.mjs +91 -0
- package/scripts/validate-step.mjs +50 -0
- package/scripts/validate-templates.mjs +226 -0
- package/scripts/validate-templates.spec.mjs +144 -0
- package/scripts/validate-workflows.mjs +106 -0
- package/scripts/workspace-portable.mjs +389 -0
- package/scripts/workspace-portable.spec.mjs +246 -0
- package/templates/README.md +47 -0
- package/templates/README.vi.md +45 -0
- package/templates/changes.example.md +27 -0
- package/templates/grammars.template.md +34 -0
- package/templates/kinds/architecture-decision.contract.json +14 -0
- package/templates/kinds/architecture-decision.skeleton.md +52 -0
- package/templates/kinds/backend-source-application.contract.json +10 -0
- package/templates/kinds/backend-source-application.skeleton.md +34 -0
- package/templates/kinds/business-promise-authority.contract.json +13 -0
- package/templates/kinds/business-promise-authority.skeleton.md +67 -0
- package/templates/kinds/capture.schema.json +33 -0
- package/templates/kinds/changes.contract.json +10 -0
- package/templates/kinds/changes.skeleton.md +26 -0
- package/templates/kinds/checks.schema.json +44 -0
- package/templates/kinds/claims.schema.json +165 -0
- package/templates/kinds/conformance.schema.json +15 -0
- package/templates/kinds/content-brief.contract.json +12 -0
- package/templates/kinds/content-brief.skeleton.md +45 -0
- package/templates/kinds/content-generation-receipt.contract.json +11 -0
- package/templates/kinds/content-generation-receipt.skeleton.md +40 -0
- package/templates/kinds/content-review.contract.json +11 -0
- package/templates/kinds/content-review.skeleton.md +45 -0
- package/templates/kinds/contract.schema.json +28 -0
- package/templates/kinds/coverage-matrix.schema.json +86 -0
- package/templates/kinds/coverage.schema.json +62 -0
- package/templates/kinds/current-state.schema.json +40 -0
- package/templates/kinds/delta.schema.json +95 -0
- package/templates/kinds/e2e.schema.json +63 -0
- package/templates/kinds/frontend-direction-decision.contract.json +93 -0
- package/templates/kinds/frontend-direction-decision.skeleton.md +68 -0
- package/templates/kinds/frontend-presentation-resolution.contract.json +11 -0
- package/templates/kinds/frontend-presentation-resolution.skeleton.md +32 -0
- package/templates/kinds/frontend-source-application.contract.json +10 -0
- package/templates/kinds/frontend-source-application.skeleton.md +30 -0
- package/templates/kinds/frontend-surface-audit.contract.json +52 -0
- package/templates/kinds/frontend-surface-audit.skeleton.md +32 -0
- package/templates/kinds/gate-result.schema.json +64 -0
- package/templates/kinds/git-publication.contract.json +82 -0
- package/templates/kinds/git-publication.skeleton.md +61 -0
- package/templates/kinds/independent-critique.contract.json +9 -0
- package/templates/kinds/independent-critique.skeleton.md +28 -0
- package/templates/kinds/inventory.schema.json +35 -0
- package/templates/kinds/model.schema.json +99 -0
- package/templates/kinds/mutations.schema.json +65 -0
- package/templates/kinds/platform-operation-receipt.contract.json +76 -0
- package/templates/kinds/platform-operation-receipt.skeleton.md +54 -0
- package/templates/kinds/probes.schema.json +130 -0
- package/templates/kinds/proof.schema.json +17 -0
- package/templates/kinds/quality-verification.contract.json +87 -0
- package/templates/kinds/quality-verification.skeleton.md +58 -0
- package/templates/kinds/release-deployment.contract.json +85 -0
- package/templates/kinds/release-deployment.skeleton.md +67 -0
- package/templates/kinds/route.schema.json +293 -0
- package/templates/kinds/stack-model.schema.json +90 -0
- package/templates/kinds/uat-capture.schema.json +35 -0
- package/templates/kinds/uat-flow-verification.contract.json +11 -0
- package/templates/kinds/uat-flow-verification.skeleton.md +47 -0
- package/templates/kinds/uat-snapshot.schema.json +96 -0
- package/templates/kinds/uat-verdicts.schema.json +41 -0
- package/templates/kinds/ui-coverage.schema.json +77 -0
- package/templates/kinds/verdicts.schema.json +39 -0
- package/templates/kinds/workspace-route-binding.contract.json +12 -0
- package/templates/kinds/workspace-route-binding.skeleton.md +60 -0
- package/templates/kinds/writes.schema.json +37 -0
- package/templates/operator.template.md +73 -0
- package/templates/patterns.template.md +31 -0
- package/templates/step/request.schema.json +42 -0
- package/templates/step/response.schema.json +144 -0
- package/templates/ui-composition.template.md +36 -0
- package/templates/ui-presentation.template.md +57 -0
- package/templates/ui-proof.template.md +34 -0
- package/workflows/README.md +37 -0
- package/workflows/README.vi.md +37 -0
- package/workflows/backend-feature.json +59 -0
- package/workflows/content-unit.json +19 -0
- package/workflows/frontend-new-surface.json +81 -0
- package/workflows/frontend-reconstruct.json +67 -0
- package/workflows/frontend-refine.json +67 -0
- package/workflows/frontend-with-uat.json +78 -0
- package/workflows/full-feature.json +104 -0
- package/workflows/release.json +29 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
// Generate the StarCi Core Grammar DNA: a compact, injectable summary of what the live package
|
|
2
|
+
// actually publishes, so a direction agent can be primed without reading the anatomy prose.
|
|
3
|
+
//
|
|
4
|
+
// Everything except the gap list is read mechanically out of `@grammar` source: package.json, the
|
|
5
|
+
// checkout head, `src/core/index.ts`, `src/core/styles.css`, `src/common/styles.css`,
|
|
6
|
+
// `src/common/renderers.ts`, and each renderer folder's own `.ts`/`.tsx`. Nothing here is inferred:
|
|
7
|
+
// a value that the regexes cannot resolve is printed as the type name, never guessed.
|
|
8
|
+
//
|
|
9
|
+
// The one knowledge input is the gap inventory: the `## Gaps` table in `family.md` (and its mirror
|
|
10
|
+
// in `family.vi.md`), whose rows are `| Component | Missing capability | Evidence |`. That table is
|
|
11
|
+
// the single home of the family's gaps, so this script copies it rather than re-deriving it; a gap
|
|
12
|
+
// recorded anywhere else is not published.
|
|
13
|
+
//
|
|
14
|
+
// Usage:
|
|
15
|
+
// node scripts/generate-grammar-dna.mjs [--grammar <path to packages/grammar>]
|
|
16
|
+
// node scripts/generate-grammar-dna.mjs --check exits 1 when the committed files differ
|
|
17
|
+
//
|
|
18
|
+
// NOT part of `npm test`: it needs the routed FE checkout on disk, which CI and a fresh clone do
|
|
19
|
+
// not have. Run it by hand after a Grammar change. `--check` ignores the generation date in the
|
|
20
|
+
// header line (only that token), so a re-run on a later day is not a false failure.
|
|
21
|
+
import { execFileSync } from 'node:child_process';
|
|
22
|
+
import { readdirSync, readFileSync, writeFileSync, existsSync, statSync } from 'node:fs';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
import process from 'node:process';
|
|
25
|
+
import { fileURLToPath } from 'node:url';
|
|
26
|
+
|
|
27
|
+
const claudeRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
28
|
+
const sourceRoot = path.resolve(claudeRoot, '..');
|
|
29
|
+
const knowledgeDir = path.join(claudeRoot, 'knowledge');
|
|
30
|
+
const outDir = path.join(knowledgeDir, 'grammars', 'starci');
|
|
31
|
+
|
|
32
|
+
const argv = process.argv.slice(2);
|
|
33
|
+
const checkOnly = argv.includes('--check');
|
|
34
|
+
const grammarFlag = argv.indexOf('--grammar');
|
|
35
|
+
|
|
36
|
+
function resolveGrammarRoot() {
|
|
37
|
+
if (grammarFlag !== -1) {
|
|
38
|
+
const given = argv[grammarFlag + 1];
|
|
39
|
+
if (!given) throw new Error('--grammar needs a path');
|
|
40
|
+
return path.resolve(given);
|
|
41
|
+
}
|
|
42
|
+
const route = path.join(sourceRoot, '.workspaces', 'local', 'routes', 'starci-academy', 'fe', 'config.json');
|
|
43
|
+
if (!existsSync(route)) throw new Error(`no routed FE checkout: ${route} is missing (pass --grammar <path>)`);
|
|
44
|
+
const diskPath = JSON.parse(readFileSync(route, 'utf8')).repository?.diskPath;
|
|
45
|
+
if (!diskPath) throw new Error(`${route}: repository.diskPath is absent`);
|
|
46
|
+
return path.join(diskPath, 'packages', 'grammar');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const grammarRoot = resolveGrammarRoot();
|
|
50
|
+
const srcDir = path.join(grammarRoot, 'src');
|
|
51
|
+
if (!existsSync(srcDir)) throw new Error(`${srcDir} is not a Grammar checkout`);
|
|
52
|
+
|
|
53
|
+
const read = (p) => readFileSync(p, 'utf8');
|
|
54
|
+
const stripComments = (text) => text.replace(/\/\*[\s\S]*?\*\//g, ' ').replace(/(^|[^:])\/\/[^\n]*/g, '$1 ');
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------- identity
|
|
57
|
+
const pkg = JSON.parse(read(path.join(grammarRoot, 'package.json')));
|
|
58
|
+
const head = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: grammarRoot, encoding: 'utf8' }).trim();
|
|
59
|
+
const coreIndex = read(path.join(srcDir, 'core', 'index.ts'));
|
|
60
|
+
const familyId = /defineGrammarFamily\(\{\s*id:\s*"([^"]+)"/.exec(coreIndex)?.[1] ?? '—';
|
|
61
|
+
const rootExport = /export const (\w*GrammarRoot)\s*=/.exec(coreIndex)?.[1] ?? '—';
|
|
62
|
+
const styleEntrypoint = /entrypoint:\s*"([^"]+)"/.exec(coreIndex)?.[1] ?? '—';
|
|
63
|
+
const scopeAttribute = /scope:\s*\{\s*attribute:\s*"([^"]+)",\s*value:\s*"([^"]+)"/.exec(coreIndex);
|
|
64
|
+
const scope = scopeAttribute ? `${scopeAttribute[1]}="${scopeAttribute[2]}"` : '—';
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------- tokens
|
|
67
|
+
// A token's default is its family-scope declaration where one exists, otherwise the fallback it is
|
|
68
|
+
// always read with. Balanced parentheses matter: `calc(100dvh - 3rem)` is one value, not a prefix.
|
|
69
|
+
function readValueAt(text, start) {
|
|
70
|
+
let depth = 0;
|
|
71
|
+
for (let i = start; i < text.length; i += 1) {
|
|
72
|
+
const ch = text[i];
|
|
73
|
+
if (ch === '(') depth += 1;
|
|
74
|
+
else if (ch === ')') {
|
|
75
|
+
if (depth === 0) return text.slice(start, i).trim();
|
|
76
|
+
depth -= 1;
|
|
77
|
+
} else if ((ch === ';' || ch === '\n') && depth === 0) return text.slice(start, i).trim();
|
|
78
|
+
}
|
|
79
|
+
return text.slice(start).trim();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const TOKEN = /--starci-core-[a-z0-9-]+/g;
|
|
83
|
+
const cssFiles = [path.join(srcDir, 'core', 'styles.css'), path.join(srcDir, 'common', 'styles.css')];
|
|
84
|
+
const tokens = new Map();
|
|
85
|
+
for (const file of cssFiles) {
|
|
86
|
+
const css = read(file);
|
|
87
|
+
for (const name of new Set(css.match(TOKEN) ?? [])) if (!tokens.has(name)) tokens.set(name, null);
|
|
88
|
+
// Declarations win over fallbacks; the first declaration in core/styles.css is the light default.
|
|
89
|
+
for (const m of css.matchAll(new RegExp(`(^|[;{\\s])(${TOKEN.source})\\s*:`, 'g'))) {
|
|
90
|
+
const name = m[2];
|
|
91
|
+
if (tokens.get(name) == null) tokens.set(name, readValueAt(css, m.index + m[0].length));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
for (const file of cssFiles) {
|
|
95
|
+
const css = read(file);
|
|
96
|
+
for (const m of css.matchAll(new RegExp(`var\\(\\s*(${TOKEN.source})\\s*,`, 'g'))) {
|
|
97
|
+
const name = m[1];
|
|
98
|
+
if (tokens.get(name) == null) tokens.set(name, readValueAt(css, m.index + m[0].length));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const tokenRows = [...tokens.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------- published rule ids
|
|
104
|
+
const HEADING = /^## ([A-Z][A-Z0-9]*(?:-[A-Z][A-Z0-9]*)*-(?:\d+|AUTO))\b/;
|
|
105
|
+
function walkMd(dir) {
|
|
106
|
+
const out = [];
|
|
107
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
108
|
+
const full = path.join(dir, entry.name);
|
|
109
|
+
if (entry.isDirectory()) out.push(...walkMd(full));
|
|
110
|
+
else if (entry.name.endsWith('.md') && !entry.name.endsWith('.vi.md')) out.push(full);
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
const publishedIds = new Set();
|
|
115
|
+
for (const file of walkMd(knowledgeDir)) {
|
|
116
|
+
for (const line of read(file).split(/\r?\n/)) {
|
|
117
|
+
const m = HEADING.exec(line);
|
|
118
|
+
if (m) publishedIds.add(m[1]);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const ID_RE = new RegExp(`\\b(?:${[...publishedIds].sort((a, b) => b.length - a.length).join('|')})\\b`, 'g');
|
|
122
|
+
|
|
123
|
+
// ---------------------------------------------------------------- renderers
|
|
124
|
+
const renderersFile = read(path.join(srcDir, 'common', 'renderers.ts'));
|
|
125
|
+
const exports = [];
|
|
126
|
+
for (const m of renderersFile.matchAll(/export \{([^}]*)\} from "([^"]+)"/g)) {
|
|
127
|
+
const modulePath = m[2].replace(/^\.\.\//, '').replace(/\.js$/, '');
|
|
128
|
+
const names = m[1].split(',').map((s) => s.trim()).filter(Boolean);
|
|
129
|
+
const components = names.filter((n) => !n.startsWith('type ') && /^[A-Z]/.test(n));
|
|
130
|
+
const types = new Set(names.filter((n) => n.startsWith('type ')).map((n) => n.slice(5).trim()));
|
|
131
|
+
for (const component of components) exports.push({ component, types, modulePath });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const KINDS = new Set(['primitive', 'composite', 'branch', 'composition']);
|
|
135
|
+
function kindOf(modulePath) {
|
|
136
|
+
const segments = modulePath.split('/');
|
|
137
|
+
const kind = segments[1];
|
|
138
|
+
return KINDS.has(kind) ? kind : segments[0];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const SKIP_TYPES = new Set([
|
|
142
|
+
'ReactNode', 'ReactElement', 'SlotContent', 'string', 'number', 'boolean', 'never', 'undefined',
|
|
143
|
+
'Key', 'CSSProperties', 'RefObject', 'unknown', 'void',
|
|
144
|
+
]);
|
|
145
|
+
const LITERAL_UNION = /^(?:"[^"]*"|\d+)(?:\s*\|\s*(?:"[^"]*"|\d+))+$/;
|
|
146
|
+
const SINGLE_NAME = /^[A-Z][A-Za-z0-9]*$/;
|
|
147
|
+
|
|
148
|
+
function folderSource(folder) {
|
|
149
|
+
let text = '';
|
|
150
|
+
for (const entry of readdirSync(folder, { withFileTypes: true })) {
|
|
151
|
+
if (!entry.isFile()) continue;
|
|
152
|
+
if (entry.name.includes('.spec.') || entry.name.includes('.test.')) continue;
|
|
153
|
+
if (!/\.(ts|tsx)$/.test(entry.name)) continue;
|
|
154
|
+
text += `${read(path.join(folder, entry.name))}\n`;
|
|
155
|
+
}
|
|
156
|
+
return text;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const unresolved = [];
|
|
160
|
+
const rendererRows = [];
|
|
161
|
+
const seen = new Set();
|
|
162
|
+
for (const { component, types, modulePath } of exports) {
|
|
163
|
+
if (seen.has(component)) continue;
|
|
164
|
+
seen.add(component);
|
|
165
|
+
// A renderer folder is `<kind>/<Name>/index`; the three renderers that are a bare file in
|
|
166
|
+
// `core/` have no folder of their own, so only that file is read.
|
|
167
|
+
const isFolder = modulePath.endsWith('/index');
|
|
168
|
+
const abs = path.join(srcDir, modulePath);
|
|
169
|
+
const raw = isFolder
|
|
170
|
+
? folderSource(path.dirname(abs))
|
|
171
|
+
: read(existsSync(`${abs}.tsx`) ? `${abs}.tsx` : `${abs}.ts`);
|
|
172
|
+
const source = stripComments(raw);
|
|
173
|
+
|
|
174
|
+
// Type aliases declared in this folder, so `tone?: BadgeTone` can resolve to its own values.
|
|
175
|
+
const aliases = new Map();
|
|
176
|
+
for (const m of source.matchAll(/^\s*(?:export\s+)?type\s+([A-Za-z0-9_]+)\s*=\s*([^\n;]+)/gm)) {
|
|
177
|
+
aliases.set(m[1], m[2].trim().replace(/\s+/g, ' '));
|
|
178
|
+
}
|
|
179
|
+
// A generic parameter (`SurfaceAccordionCardItem<Summary, Body>`) is a caller's type, not a
|
|
180
|
+
// published value set; it is never printed as a prop type.
|
|
181
|
+
const generics = new Set();
|
|
182
|
+
for (const m of source.matchAll(/\b(?:type|interface)\s+[A-Za-z0-9_]+\s*<([^>]*)>/g)) {
|
|
183
|
+
for (const part of m[1].split(',')) {
|
|
184
|
+
const name = part.trim().split(/\s+/)[0];
|
|
185
|
+
if (name) generics.add(name);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const resolve = (text, depth = 0) => {
|
|
189
|
+
const clean = text.trim().replace(/\s+/g, ' ');
|
|
190
|
+
if (LITERAL_UNION.test(clean)) return { values: clean.split('|').map((v) => v.trim().replace(/"/g, '')) };
|
|
191
|
+
if (depth < 3 && SINGLE_NAME.test(clean) && aliases.has(clean)) return resolve(aliases.get(clean), depth + 1);
|
|
192
|
+
return { name: clean };
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// Props: every `readonly name?: <type>` in the folder, so intersection members count too.
|
|
196
|
+
const props = new Map();
|
|
197
|
+
for (const m of source.matchAll(/readonly\s+([A-Za-z0-9_]+)\??\s*:\s*([^;\n},]+(?:\|[^;\n},]+)*)/g)) {
|
|
198
|
+
const [, name, typeText] = m;
|
|
199
|
+
const resolved = resolve(typeText);
|
|
200
|
+
if (resolved.values) {
|
|
201
|
+
const prior = props.get(name);
|
|
202
|
+
if (!prior || prior.startsWith('`')) props.set(name, resolved.values.map((v) => `\`${v}\``).join(' \\| '));
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (props.has(name)) continue;
|
|
206
|
+
if (SKIP_TYPES.has(resolved.name) || generics.has(resolved.name) || !SINGLE_NAME.test(resolved.name)) continue;
|
|
207
|
+
props.set(name, resolved.name);
|
|
208
|
+
unresolved.push(`${component}.${name}: ${resolved.name}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Claims: ids inside a literal `data-contract` attribute are static; every other published id in
|
|
212
|
+
// the folder reaches the DOM only through a map or helper, so it is reported as computed.
|
|
213
|
+
const staticIds = new Set();
|
|
214
|
+
for (const m of source.matchAll(/data-contract"?\s*[:=]\s*(?:\{\s*)?"([^"]*)"/g)) {
|
|
215
|
+
for (const id of m[1].match(ID_RE) ?? []) staticIds.add(id);
|
|
216
|
+
}
|
|
217
|
+
const allIds = new Set(source.match(ID_RE) ?? []);
|
|
218
|
+
const computed = [...allIds].filter((id) => !staticIds.has(id)).sort();
|
|
219
|
+
const claims = [
|
|
220
|
+
...[...staticIds].sort(),
|
|
221
|
+
...(computed.length > 0 ? [`computed: ${computed.join(' ')}`] : []),
|
|
222
|
+
];
|
|
223
|
+
|
|
224
|
+
const classes = [...new Set(source.match(/(?<![-\w])starci-core-[a-z0-9-]+/g) ?? [])].sort();
|
|
225
|
+
const propsType = [...types].find((t) => t === `${component}Props`) ?? '—';
|
|
226
|
+
|
|
227
|
+
rendererRows.push({
|
|
228
|
+
component,
|
|
229
|
+
kind: kindOf(modulePath),
|
|
230
|
+
propsType,
|
|
231
|
+
props: [...props.entries()].map(([n, v]) => `${n}: ${v}`),
|
|
232
|
+
claims,
|
|
233
|
+
classes,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ---------------------------------------------------------------- gaps
|
|
238
|
+
function splitCells(line) {
|
|
239
|
+
const trimmed = line.trim().replace(/^\|/, '').replace(/\|$/, '');
|
|
240
|
+
const cells = [];
|
|
241
|
+
let cur = '';
|
|
242
|
+
for (let i = 0; i < trimmed.length; i += 1) {
|
|
243
|
+
const ch = trimmed[i];
|
|
244
|
+
if (ch === '\\') { cur += ch + (trimmed[i + 1] ?? ''); i += 1; continue; }
|
|
245
|
+
if (ch === '|') { cells.push(cur.trim()); cur = ''; continue; }
|
|
246
|
+
cur += ch;
|
|
247
|
+
}
|
|
248
|
+
cells.push(cur.trim());
|
|
249
|
+
return cells;
|
|
250
|
+
}
|
|
251
|
+
// The gap table is the first table in `family.md` whose header row opens with `| Component |`; its
|
|
252
|
+
// body runs until the first line that is no longer a table row.
|
|
253
|
+
function readGaps(suffix) {
|
|
254
|
+
const file = path.join(outDir, `family${suffix}`);
|
|
255
|
+
if (!existsSync(file)) throw new Error(`${file} is missing: it owns the gap inventory`);
|
|
256
|
+
const lines = read(file).split(/\r?\n/);
|
|
257
|
+
const start = lines.findIndex((line) => /^\|\s*Component\s*\|/.test(line.trim()));
|
|
258
|
+
if (start === -1) throw new Error(`${file}: no gap table (a header row opening with "| Component |")`);
|
|
259
|
+
const rows = [];
|
|
260
|
+
for (let i = start + 2; i < lines.length; i += 1) {
|
|
261
|
+
if (!lines[i].trim().startsWith('|')) break;
|
|
262
|
+
const cells = splitCells(lines[i]);
|
|
263
|
+
rows.push({ component: cells[0], clause: cells[1], evidence: cells[2] ?? '—' });
|
|
264
|
+
}
|
|
265
|
+
return rows;
|
|
266
|
+
}
|
|
267
|
+
const gapRows = readGaps('.md');
|
|
268
|
+
// The Vietnamese mirror carries the same rows in the house voice, in the same order. A mirror that
|
|
269
|
+
// has not caught up prints the English row rather than nothing.
|
|
270
|
+
const viGaps = readGaps('.vi.md');
|
|
271
|
+
gapRows.forEach((row, i) => { row.vi = viGaps[i] ?? row; });
|
|
272
|
+
|
|
273
|
+
// ---------------------------------------------------------------- render
|
|
274
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
275
|
+
const headerLine = (en) => en
|
|
276
|
+
? `\`${pkg.name}@${pkg.version}\` · checkout \`${head}\` · generated ${today}`
|
|
277
|
+
: `\`${pkg.name}@${pkg.version}\` · checkout \`${head}\` · sinh ngày ${today}`;
|
|
278
|
+
|
|
279
|
+
function renderers(en) {
|
|
280
|
+
const header = en
|
|
281
|
+
? '| Renderer | Kind | Props with closed values | Claims | Classes |'
|
|
282
|
+
: '| Renderer | Loại | Prop có tập giá trị đóng | Claim | Class |';
|
|
283
|
+
const rows = rendererRows.map((r) => `| \`${r.component}\` \`${r.propsType}\` | ${r.kind} | ${r.props.length > 0 ? r.props.join('; ') : '—'} | ${r.claims.length > 0 ? r.claims.join(' ') : '—'} | ${r.classes.length > 0 ? r.classes.map((c) => `\`${c}\``).join(' ') : '—'} |`);
|
|
284
|
+
return [header, '| --- | --- | --- | --- | --- |', ...rows].join('\n');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function document(en) {
|
|
288
|
+
const t = en
|
|
289
|
+
? {
|
|
290
|
+
title: '# StarCi Core — DNA',
|
|
291
|
+
intro: `This file is generated from the live \`@starci/grammar\` package, never written by hand: identity, tokens, renderers, published props, \`data-contract\` claims, and emitted classes are read out of \`src/\`, and the gap list is copied from the \`## Gaps\` table in [Family and DNA](family.md). Prime a direction agent with this file: it says what exists. [Idioms](idioms.md) says how StarCi composes it, and [Playbook](playbook.md) says which idioms a business shape needs. Regenerate with \`node scripts/generate-grammar-dna.mjs\`, and verify with \`--check\`; it needs the routed FE checkout, so it is not part of \`npm test\`.`,
|
|
292
|
+
identity: '## Identity',
|
|
293
|
+
identityHeader: '| Fact | Value |',
|
|
294
|
+
tokens: '## Tokens',
|
|
295
|
+
tokensHeader: '| Token | Default |',
|
|
296
|
+
renderersHead: '## Renderers',
|
|
297
|
+
gaps: '## Gaps',
|
|
298
|
+
gapsHeader: '| Component | Missing capability | Evidence |',
|
|
299
|
+
facts: [
|
|
300
|
+
['Package', `\`${pkg.name}\``],
|
|
301
|
+
['Version', `\`${pkg.version}\``],
|
|
302
|
+
['Checkout head', `\`${head}\``],
|
|
303
|
+
['Family id', `\`${familyId}\``],
|
|
304
|
+
['Root component', `\`${rootExport}\``],
|
|
305
|
+
['Family scope', `\`${scope}\``],
|
|
306
|
+
['Style entrypoint', `\`${styleEntrypoint}\``],
|
|
307
|
+
['Counts', `${rendererRows.length} renderers · ${tokenRows.length} tokens · ${rendererRows.reduce((n, r) => n + r.claims.length, 0)} claim entries · ${gapRows.length} gaps`],
|
|
308
|
+
],
|
|
309
|
+
}
|
|
310
|
+
: {
|
|
311
|
+
title: '# StarCi Core — DNA',
|
|
312
|
+
intro: `File này được sinh ra từ package \`@starci/grammar\` đang chạy, không viết tay: danh tính, token, renderer, prop công bố, claim \`data-contract\` và class phát ra đều đọc thẳng từ \`src/\`, còn danh sách gap được chép từ bảng \`## Gap\` trong [Family và DNA](family.vi.md). Hãy mồi cho agent định hướng bằng đúng file này: nó nói cái gì đang tồn tại. [Idiom](idioms.vi.md) nói StarCi ghép chúng ra sao, còn [Playbook](playbook.vi.md) nói hình dạng nghiệp vụ nào cần chuỗi idiom nào. Sinh lại bằng \`node scripts/generate-grammar-dna.mjs\`, kiểm bằng \`--check\`; lệnh này cần checkout FE đã định tuyến nên không nằm trong \`npm test\`.`,
|
|
313
|
+
identity: '## Danh tính',
|
|
314
|
+
identityHeader: '| Dữ kiện | Giá trị |',
|
|
315
|
+
tokens: '## Token',
|
|
316
|
+
tokensHeader: '| Token | Mặc định |',
|
|
317
|
+
renderersHead: '## Renderer',
|
|
318
|
+
gaps: '## Gap',
|
|
319
|
+
gapsHeader: '| Component | Năng lực còn thiếu | Bằng chứng |',
|
|
320
|
+
facts: [
|
|
321
|
+
['Package', `\`${pkg.name}\``],
|
|
322
|
+
['Phiên bản', `\`${pkg.version}\``],
|
|
323
|
+
['Head của checkout', `\`${head}\``],
|
|
324
|
+
['Family id', `\`${familyId}\``],
|
|
325
|
+
['Component gốc', `\`${rootExport}\``],
|
|
326
|
+
['Phạm vi family', `\`${scope}\``],
|
|
327
|
+
['Điểm vào CSS', `\`${styleEntrypoint}\``],
|
|
328
|
+
['Số đếm', `${rendererRows.length} renderer · ${tokenRows.length} token · ${rendererRows.reduce((n, r) => n + r.claims.length, 0)} mục claim · ${gapRows.length} gap`],
|
|
329
|
+
],
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
return [
|
|
333
|
+
t.title,
|
|
334
|
+
'',
|
|
335
|
+
headerLine(en),
|
|
336
|
+
'',
|
|
337
|
+
t.intro,
|
|
338
|
+
'',
|
|
339
|
+
t.identity,
|
|
340
|
+
'',
|
|
341
|
+
t.identityHeader,
|
|
342
|
+
'| --- | --- |',
|
|
343
|
+
...t.facts.map(([k, v]) => `| ${k} | ${v} |`),
|
|
344
|
+
'',
|
|
345
|
+
t.tokens,
|
|
346
|
+
'',
|
|
347
|
+
t.tokensHeader,
|
|
348
|
+
'| --- | --- |',
|
|
349
|
+
...tokenRows.map(([name, value]) => `| \`${name}\` | \`${value ?? '—'}\` |`),
|
|
350
|
+
'',
|
|
351
|
+
t.renderersHead,
|
|
352
|
+
'',
|
|
353
|
+
renderers(en),
|
|
354
|
+
'',
|
|
355
|
+
t.gaps,
|
|
356
|
+
'',
|
|
357
|
+
t.gapsHeader,
|
|
358
|
+
'| --- | --- | --- |',
|
|
359
|
+
...gapRows.map((g) => (en
|
|
360
|
+
? `| ${g.component} | ${g.clause} | ${g.evidence} |`
|
|
361
|
+
: `| ${g.vi.component} | ${g.vi.clause} | ${g.vi.evidence} |`)),
|
|
362
|
+
'',
|
|
363
|
+
].join('\n');
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const outputs = [
|
|
367
|
+
[path.join(outDir, 'DNA.md'), document(true)],
|
|
368
|
+
[path.join(outDir, 'DNA.vi.md'), document(false)],
|
|
369
|
+
];
|
|
370
|
+
|
|
371
|
+
// The generation date is the one token allowed to drift, so --check stays honest across days.
|
|
372
|
+
const normalise = (text) => text.replace(/(generated|sinh ngày) \d{4}-\d{2}-\d{2}/, '$1 <date>');
|
|
373
|
+
|
|
374
|
+
if (checkOnly) {
|
|
375
|
+
const drifted = outputs.filter(([file, text]) => !existsSync(file) || normalise(read(file)) !== normalise(text));
|
|
376
|
+
if (drifted.length > 0) {
|
|
377
|
+
process.stderr.write(`grammar DNA is stale: ${drifted.map(([f]) => path.relative(claudeRoot, f)).join(', ')}\n`);
|
|
378
|
+
process.exitCode = 1;
|
|
379
|
+
} else {
|
|
380
|
+
process.stdout.write(`grammar DNA current: ${rendererRows.length} renderers, ${tokenRows.length} tokens, ${gapRows.length} gaps\n`);
|
|
381
|
+
}
|
|
382
|
+
} else {
|
|
383
|
+
for (const [file, text] of outputs) writeFileSync(file, text, 'utf8');
|
|
384
|
+
const claimCount = rendererRows.reduce((n, r) => n + r.claims.length, 0);
|
|
385
|
+
process.stdout.write(`wrote DNA.md and DNA.vi.md: ${tokenRows.length} tokens, ${rendererRows.length} renderers, ${claimCount} claim entries, ${gapRows.length} gaps\n`);
|
|
386
|
+
if (unresolved.length > 0) process.stdout.write(`props printed as a type name (no literal union in the renderer's own folder): ${unresolved.join(', ')}\n`);
|
|
387
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// operators/INDEX.md is generated from every operator.md: the Context table (static reads), the Inputs
|
|
2
|
+
// and Outputs tables (kinds that cross between branches), the Steps table (count) and the Stops table
|
|
3
|
+
// (codes). It is the one table that says, for every operator, what it needs, what it produces, and who
|
|
4
|
+
// consumes it. `--check` regenerates in memory and fails when the committed file differs.
|
|
5
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import process from 'node:process';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { loadOperatorPackages, cellCodes, cellAliases, kindOf, isYes } from './operator-md.mjs';
|
|
10
|
+
import { loadErrorsRegistry } from './errors-registry.mjs';
|
|
11
|
+
|
|
12
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
13
|
+
const packages = (await loadOperatorPackages(root)).filter((p) => p.shape === 'v9');
|
|
14
|
+
const registry = await loadErrorsRegistry(root);
|
|
15
|
+
if (registry.errors.length) { process.stderr.write(`${registry.errors.join('\n')}\n`); process.exit(1); }
|
|
16
|
+
const ops = packages.map((p) => {
|
|
17
|
+
const op = p.en;
|
|
18
|
+
const reads = new Set();
|
|
19
|
+
for (const r of op.tables.context?.rows ?? []) for (const a of cellAliases(r.alias)) reads.add(a);
|
|
20
|
+
return {
|
|
21
|
+
id: p.manifest.id, job: op.job || p.manifest.job, profile: p.manifest.resources?.profile ?? '?',
|
|
22
|
+
steps: op.tables.steps?.rows.length ?? 0,
|
|
23
|
+
codes: (op.tables.stops?.rows ?? []).map((r) => cellCodes(r.code)[0] ?? r.code.trim()),
|
|
24
|
+
context: [...reads].sort(),
|
|
25
|
+
inputs: (op.tables.inputs?.rows ?? []).map((r) => ({ kind: kindOf(r.kind), required: isYes(r.required) })).filter((i) => i.kind && i.kind !== '—'),
|
|
26
|
+
outputs: (op.tables.outputs?.rows ?? []).map((r) => ({ kind: kindOf(r.kind), file: r.file.replace(/`/g, ''), type: r.type.trim(), required: isYes(r.required) })),
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Who produces and who consumes each kind.
|
|
31
|
+
const producers = new Map();
|
|
32
|
+
for (const op of ops) for (const o of op.outputs) producers.set(o.kind, [...(producers.get(o.kind) ?? []), op.id]);
|
|
33
|
+
const consumers = new Map();
|
|
34
|
+
for (const op of ops) for (const i of op.inputs) consumers.set(i.kind, [...(consumers.get(i.kind) ?? []), `${op.id}${i.required ? '' : ' (optional)'}`]);
|
|
35
|
+
|
|
36
|
+
const code = (s) => `\`${s}\``;
|
|
37
|
+
const list = (arr) => (arr.length ? arr.map(code).join(', ') : '—');
|
|
38
|
+
|
|
39
|
+
function render(lang) {
|
|
40
|
+
const t = lang === 'en' ? {
|
|
41
|
+
title: '# Operators',
|
|
42
|
+
intro: `Generated by \`scripts/generate-operators-index.mjs\` from every \`operator.md\`; \`--check\` runs inside \`npm test\`, so this table cannot drift from the packages. Context aliases exist before the session; kinds are produced inside a branch (\`step-N/parallel-M/response/\`) and handed to a later branch by an explicit path in its \`request.json\`. ${ops.length} operators.`,
|
|
43
|
+
h1: '## What each operator needs and produces', c1: '| Operator | Profile | Context | Inputs (kinds) | Outputs (kinds) | Steps | Stop codes |',
|
|
44
|
+
h2: '## Hand-offs between branches', c2: '| Kind | Produced by | Consumed by |', intro2: 'Every kind that crosses between operators, who writes it, and who reads it. A kind with no consumer is kept for audit or for a person; a kind with no producer is a defect this check reports.',
|
|
45
|
+
h3: '## Jobs', c3: '| Operator | Single job |',
|
|
46
|
+
} : {
|
|
47
|
+
title: '# Các operator',
|
|
48
|
+
intro: `Sinh bởi \`scripts/generate-operators-index.mjs\` từ mọi \`operator.md\`; \`--check\` chạy trong \`npm test\`, nên bảng này không thể lệch với các gói. Alias trong Context có trước phiên; kind được sinh trong một nhánh (\`step-N/parallel-M/response/\`) và bàn giao cho nhánh sau bằng đường dẫn tường minh trong \`request.json\` của nhánh đó. ${ops.length} operator.`,
|
|
49
|
+
h1: '## Mỗi operator cần gì và tạo ra gì', c1: '| Operator | Profile | Context | Đầu vào (kind) | Đầu ra (kind) | Số bước | Mã dừng |',
|
|
50
|
+
h2: '## Bàn giao giữa các nhánh', c2: '| Kind | Do ai sinh | Ai tiêu thụ |', intro2: 'Mọi kind đi qua giữa các operator, ai ghi nó, ai đọc nó. Kind không có người tiêu thụ là giữ cho audit hoặc cho người; kind không có người sinh là lỗi mà bước kiểm sẽ báo.',
|
|
51
|
+
h3: '## Việc duy nhất', c3: '| Operator | Việc duy nhất |',
|
|
52
|
+
};
|
|
53
|
+
const out = [t.title, '', t.intro, '', t.h1, '', t.c1, '| --- | --- | --- | --- | --- | --- | --- |'];
|
|
54
|
+
for (const op of ops) out.push(`| ${code(op.id)} | ${code(op.profile)} | ${list(op.context)} | ${list(op.inputs.map((i) => i.kind))} | ${list(op.outputs.map((o) => o.kind))} | ${op.steps} | ${list(op.codes)} |`);
|
|
55
|
+
out.push('', t.h2, '', t.intro2, '', t.c2, '| --- | --- | --- |');
|
|
56
|
+
const kinds = new Set([...producers.keys(), ...consumers.keys()]);
|
|
57
|
+
for (const k of [...kinds].sort()) out.push(`| ${code(k)} | ${list(producers.get(k) ?? [])} | ${list(consumers.get(k) ?? [])} |`);
|
|
58
|
+
out.push('', t.h3, '', t.c3, '| --- | --- |');
|
|
59
|
+
for (const op of ops) out.push(`| ${code(op.id)} | ${op.job} |`);
|
|
60
|
+
// Stop codes: the merged registry (operators/errors.json + every operators/<id>/errors.json).
|
|
61
|
+
const s = lang === 'en' ? {
|
|
62
|
+
h: '## Stop codes', intro: 'Every code an operator may stop with, merged from `operators/errors.json` (codes several operators share, with a scope list) and each `operators/<id>/errors.json`. A code has exactly one disposition: **terminate** ends the branch blocked; **fallback** performs the named action, records it under `## Fallbacks taken` in `response.md`, and continues. `unless` names the one Requirements param whose value flips the disposition. `domain` is the `routing.json` domain the stop hands to; `self` is the emitting operator\'s own domain, a resume. A runtime meeting an unlisted code terminates with `UNKNOWN_STOP`.',
|
|
63
|
+
head: '| Code | Scope | Domain | Disposition | Meaning | Fallback | Unless | Resume |',
|
|
64
|
+
} : {
|
|
65
|
+
h: '## Mã dừng', intro: 'Mọi mã một operator có thể dừng với, gộp từ `operators/errors.json` (mã nhiều operator dùng chung, có danh sách scope) và từng `operators/<id>/errors.json`. Một mã có đúng một cách xử lý: **terminate** kết thúc nhánh ở trạng thái blocked; **fallback** làm đúng hành động đã ghi, ghi lại dưới `## Fallbacks taken` trong `response.md`, rồi chạy tiếp. `unless` gọi tên đúng một tham số Yêu cầu mà giá trị của nó đảo cách xử lý. `domain` là vùng trong `routing.json` mà mã dừng bàn giao tới; `self` là vùng của chính operator phát mã, tức chạy lại. Runtime gặp mã không có trong sổ thì dừng với `UNKNOWN_STOP`.',
|
|
66
|
+
head: '| Mã | Phạm vi | Vùng | Xử lý | Nghĩa | Fallback | Trừ khi | Chạy lại |',
|
|
67
|
+
};
|
|
68
|
+
out.push('', s.h, '', s.intro, '', s.head, '| --- | --- | --- | --- | --- | --- | --- | --- |');
|
|
69
|
+
const esc = (v) => String(v).replace(/\|/g, '\\|');
|
|
70
|
+
const scopeKey = (c) => (c.scope.includes('*') ? '' : c.scope.join(','));
|
|
71
|
+
const entries = Object.entries(registry.codes).sort(([a, x], [b, y]) => scopeKey(x).localeCompare(scopeKey(y)) || a.localeCompare(b));
|
|
72
|
+
for (const [id, c] of entries) {
|
|
73
|
+
const unless = c.unless ? `${code(c.unless.param)} = ${code(c.unless.equals)} → ${c.unless.then}` : '—';
|
|
74
|
+
out.push(`| ${code(id)} | ${c.scope.map(code).join(', ')} | ${code(c.domain)} | ${c.disposition} | ${esc(c.meaning[lang])} | ${c.fallback ? esc(c.fallback[lang]) : '—'} | ${unless} | ${esc(c.resume?.[lang] ?? '—')} |`);
|
|
75
|
+
}
|
|
76
|
+
return `${out.join('\n')}\n`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const targets = [['operators/INDEX.md', 'en'], ['operators/INDEX.vi.md', 'vi']];
|
|
80
|
+
const check = process.argv.includes('--check');
|
|
81
|
+
let drift = 0;
|
|
82
|
+
for (const [rel, lang] of targets) {
|
|
83
|
+
const next = render(lang);
|
|
84
|
+
const file = path.join(root, rel);
|
|
85
|
+
if (check) {
|
|
86
|
+
let current = '';
|
|
87
|
+
try { current = (await readFile(file, 'utf8')).replace(/\r\n/g, '\n'); } catch { current = ''; }
|
|
88
|
+
if (current !== next) { drift += 1; process.stderr.write(`${rel}: out of date; run node scripts/generate-operators-index.mjs\n`); }
|
|
89
|
+
} else await writeFile(file, next);
|
|
90
|
+
}
|
|
91
|
+
// A kind somebody consumes but nobody produces is a broken hand-off.
|
|
92
|
+
const orphans = [...consumers.keys()].filter((k) => !producers.has(k));
|
|
93
|
+
if (orphans.length) { process.stderr.write(`kinds consumed but produced by no operator: ${orphans.join(', ')}\n`); drift += 1; }
|
|
94
|
+
if (drift) process.exitCode = 1;
|
|
95
|
+
else process.stdout.write(`${check ? 'operators index current' : 'operators index written'}: ${ops.length} operators, ${producers.size} kinds\n`);
|