@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,185 @@
|
|
|
1
|
+
// Proves validate.mjs on a synthetic session branch: one conforming audit over two matrix entries
|
|
2
|
+
// (one all-pass entry, one entry whose application-owned node fails and routes back to resolve), one
|
|
3
|
+
// blocked on EVIDENCE_MISSING with nothing captured, and one mutation per law, each of which must
|
|
4
|
+
// fail with a line that names the defect.
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
7
|
+
import { tmpdir } from 'node:os';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { validateAuditStep } from './validate.mjs';
|
|
10
|
+
|
|
11
|
+
const WIDE = 'wide-light-loaded';
|
|
12
|
+
const NARROW = 'narrow-light-loaded';
|
|
13
|
+
const MAIN = 'body>main';
|
|
14
|
+
const SECTION = 'body>main>section';
|
|
15
|
+
const CAP = (id) => `response/data/captures/${id}.json`;
|
|
16
|
+
const SHOT = (id) => `response/artifacts/${id}.png`;
|
|
17
|
+
|
|
18
|
+
const capture = (id) => ({
|
|
19
|
+
matrixId: id,
|
|
20
|
+
viewport: id === WIDE ? [1440, 900] : [390, 844],
|
|
21
|
+
scheme: 'light',
|
|
22
|
+
state: 'loaded',
|
|
23
|
+
nodes: [
|
|
24
|
+
{ path: MAIN, owner: 'app', claims: ['GAP-5'], measured: { gap: id === WIDE ? '1.5rem' : '1rem' } },
|
|
25
|
+
{ path: SECTION, owner: 'grammar', claims: ['PADDING-4'], measured: { padding: '1rem' } },
|
|
26
|
+
],
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const verdicts = () => ({
|
|
30
|
+
entries: [
|
|
31
|
+
{
|
|
32
|
+
matrixId: WIDE,
|
|
33
|
+
results: [
|
|
34
|
+
{ path: MAIN, owner: 'app', rule: 'GAP-5', measured: '1.5rem', verdict: 'pass', routeTo: 'none' },
|
|
35
|
+
{ path: SECTION, owner: 'grammar', rule: 'PADDING-4', measured: '1rem', verdict: 'pass', routeTo: 'none' },
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
matrixId: NARROW,
|
|
40
|
+
results: [
|
|
41
|
+
{ path: MAIN, owner: 'app', rule: 'GAP-5', measured: '1rem', verdict: 'fail', routeTo: 'resolve' },
|
|
42
|
+
{ path: SECTION, owner: 'grammar', rule: 'PADDING-4', measured: '1rem', verdict: 'pass', routeTo: 'none' },
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const responseMd = () => `# frontend-surface-audit — plan-picker
|
|
49
|
+
|
|
50
|
+
## Matrix
|
|
51
|
+
|
|
52
|
+
| Matrix | Viewport | Scheme | State | Screenshot |
|
|
53
|
+
| --- | --- | --- | --- | --- |
|
|
54
|
+
| \`${WIDE}\` | 1440x900 | light | loaded | \`${SHOT(WIDE)}\` |
|
|
55
|
+
| \`${NARROW}\` | 390x844 | light | loaded | \`${SHOT(NARROW)}\` |
|
|
56
|
+
|
|
57
|
+
## Verdicts by owner
|
|
58
|
+
|
|
59
|
+
| Matrix | Owner | Node | Rule | Measured | Verdict |
|
|
60
|
+
| --- | --- | --- | --- | --- | --- |
|
|
61
|
+
| \`${WIDE}\` | app | \`${MAIN}\` | \`GAP-5\` | 1.5rem | pass |
|
|
62
|
+
| \`${WIDE}\` | grammar | \`${SECTION}\` | \`PADDING-4\` | 1rem | pass |
|
|
63
|
+
| \`${NARROW}\` | app | \`${MAIN}\` | \`GAP-5\` | 1rem | fail |
|
|
64
|
+
| \`${NARROW}\` | grammar | \`${SECTION}\` | \`PADDING-4\` | 1rem | pass |
|
|
65
|
+
|
|
66
|
+
## Regressions
|
|
67
|
+
|
|
68
|
+
| Matrix | Node | Rule | Measured | Routes to |
|
|
69
|
+
| --- | --- | --- | --- | --- |
|
|
70
|
+
| \`${NARROW}\` | \`${MAIN}\` | \`GAP-5\` | 1rem | resolve |
|
|
71
|
+
|
|
72
|
+
## Grammar gaps
|
|
73
|
+
|
|
74
|
+
| Component | Rule | What the family lacks |
|
|
75
|
+
| --- | --- | --- |
|
|
76
|
+
|
|
77
|
+
## Fallbacks taken
|
|
78
|
+
|
|
79
|
+
| Code | Action |
|
|
80
|
+
| --- | --- |
|
|
81
|
+
`;
|
|
82
|
+
|
|
83
|
+
const requestJson = ({ extra = {} } = {}) => ({
|
|
84
|
+
schemaVersion: 9,
|
|
85
|
+
operatorId: 'frontend.surface.audit',
|
|
86
|
+
step: 4,
|
|
87
|
+
parallel: 1,
|
|
88
|
+
sessionId: 's-test',
|
|
89
|
+
contexts: [{ alias: '@knowledge/ui/proof', head: null }, { alias: '@workspaces/fe', head: 'e'.repeat(40) }, { alias: '@worktrees/sessions/central-runtime', head: null }],
|
|
90
|
+
requirements: { matrix: [], readinessProbe: 'route-served', resume: null, ...extra },
|
|
91
|
+
inputs: {
|
|
92
|
+
'frontend-source-application': 'step-3/parallel-1/response/response.md',
|
|
93
|
+
'frontend-presentation-resolution': 'step-2/parallel-1/response/response.md',
|
|
94
|
+
'frontend-direction-decision': 'step-1/parallel-1/response/response.md',
|
|
95
|
+
},
|
|
96
|
+
resume: null,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const responseJson = ({ status = 'done', stop, fields, next = ['frontend.presentation.resolve'] } = {}) => ({
|
|
100
|
+
schemaVersion: 9,
|
|
101
|
+
operatorId: 'frontend.surface.audit',
|
|
102
|
+
step: 4,
|
|
103
|
+
parallel: 1,
|
|
104
|
+
status,
|
|
105
|
+
...(stop ? { stop } : {}),
|
|
106
|
+
fallbacks: [],
|
|
107
|
+
fields: fields ?? {
|
|
108
|
+
'frontend-surface-audit': 'response/response.md',
|
|
109
|
+
verdicts: 'response/data/verdicts.json',
|
|
110
|
+
capture: [CAP(WIDE), CAP(NARROW)],
|
|
111
|
+
screenshot: [SHOT(WIDE), SHOT(NARROW)],
|
|
112
|
+
},
|
|
113
|
+
commits: [],
|
|
114
|
+
next,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
function writeBranch(files) {
|
|
118
|
+
const session = mkdtempSync(path.join(tmpdir(), 'fe-audit-session-'));
|
|
119
|
+
const branch = path.join(session, 'step-4', 'parallel-1');
|
|
120
|
+
for (const d of ['request', 'response/data/captures', 'response/artifacts']) mkdirSync(path.join(branch, d), { recursive: true });
|
|
121
|
+
for (const [step, name] of [[1, 'frontend-direction-decision'], [2, 'frontend-presentation-resolution'], [3, 'frontend-source-application']]) {
|
|
122
|
+
const dir = path.join(session, `step-${step}`, 'parallel-1', 'response');
|
|
123
|
+
mkdirSync(dir, { recursive: true });
|
|
124
|
+
writeFileSync(path.join(dir, 'response.md'), `# ${name} — plan-picker\n`);
|
|
125
|
+
}
|
|
126
|
+
writeFileSync(path.join(session, 'state.json'), JSON.stringify({ id: 's-test', chain: [['3/1'], ['4/1']], steps: { '3/1': 'frontend.source.apply', '4/1': 'frontend.surface.audit' }, current: '4/1', status: 'running' }));
|
|
127
|
+
for (const [name, content] of Object.entries(files)) {
|
|
128
|
+
if (content === null) continue;
|
|
129
|
+
writeFileSync(path.join(branch, name), typeof content === 'string' ? content : JSON.stringify(content, null, 2));
|
|
130
|
+
}
|
|
131
|
+
return { branch, session };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const baseline = () => ({
|
|
135
|
+
'request/request.json': requestJson(),
|
|
136
|
+
'response/response.json': responseJson(),
|
|
137
|
+
'response/response.md': responseMd(),
|
|
138
|
+
'response/data/verdicts.json': verdicts(),
|
|
139
|
+
[CAP(WIDE)]: capture(WIDE),
|
|
140
|
+
[CAP(NARROW)]: capture(NARROW),
|
|
141
|
+
[SHOT(WIDE)]: 'PNG',
|
|
142
|
+
[SHOT(NARROW)]: 'PNG',
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
async function expectValid(files, label) {
|
|
146
|
+
const { branch, session } = writeBranch(files);
|
|
147
|
+
const { errors } = await validateAuditStep(branch);
|
|
148
|
+
rmSync(session, { recursive: true, force: true });
|
|
149
|
+
assert.deepEqual(errors, [], `${label} should be valid`);
|
|
150
|
+
}
|
|
151
|
+
async function expectError(files, needle, label) {
|
|
152
|
+
const { branch, session } = writeBranch(files);
|
|
153
|
+
const { errors } = await validateAuditStep(branch);
|
|
154
|
+
rmSync(session, { recursive: true, force: true });
|
|
155
|
+
assert.ok(errors.some((e) => e.includes(needle)), `${label}: expected an error containing "${needle}", got:\n${errors.join('\n') || '(none)'}`);
|
|
156
|
+
}
|
|
157
|
+
const mutate = (change) => { const v = verdicts(); change(v); return { ...baseline(), 'response/data/verdicts.json': v }; };
|
|
158
|
+
|
|
159
|
+
await expectValid(baseline(), 'two entries: one all-pass, one application-owned failure routed back to resolve');
|
|
160
|
+
await expectValid({ 'request/request.json': requestJson(), 'response/response.json': responseJson({ status: 'blocked', stop: 'EVIDENCE_MISSING', next: [], fields: {} }) }, 'blocked on EVIDENCE_MISSING with nothing captured');
|
|
161
|
+
|
|
162
|
+
await expectError({ ...baseline(), 'request/request.json': requestJson({ extra: { mystery: 1 } }) }, 'requirements.mystery is not a field', 'undeclared requirement');
|
|
163
|
+
await expectError({ ...baseline(), 'request/request.json': requestJson({ extra: { matrix: [{ matrixId: WIDE }] } }) }, 'the request narrowed the matrix without it', 'entry judged outside the narrowed matrix');
|
|
164
|
+
await expectError(mutate((v) => { v.entries[1].results[0].routeTo = 'none'; }), 'fails and routes nowhere', 'failure that routes nowhere');
|
|
165
|
+
await expectError(mutate((v) => { v.entries[1].results[0].routeTo = 'grammar-gap'; }), 'application-owned and its failure must route to resolve', 'application failure sent to the family');
|
|
166
|
+
await expectError(mutate((v) => { v.entries[1].results[1].verdict = 'fail'; v.entries[1].results[1].routeTo = 'resolve'; }), "a failure there is a grammar-gap, never a resolve loop", 'Grammar failure sent into the resolve loop');
|
|
167
|
+
await expectError(mutate((v) => { v.entries[0].results[0].routeTo = 'resolve'; }), 'passes and still routes to resolve', 'passing node that routes somewhere');
|
|
168
|
+
await expectError(mutate((v) => { v.entries[1].results[0].rule = 'GAP-4'; }), 'which that node never claimed', 'judged on a rule nobody claimed');
|
|
169
|
+
await expectError(mutate((v) => { v.entries[0].results[0].measured = '2rem'; }), 'which the capture did not measure', 'judged against a value nobody measured');
|
|
170
|
+
await expectError(mutate((v) => { v.entries[0].results[0].owner = 'grammar'; }), 'in the capture', 'owner differs from the capture');
|
|
171
|
+
await expectError(mutate((v) => { v.entries[0].results.push({ path: 'body>footer', owner: 'app', rule: 'GAP-1', measured: '0.25rem', verdict: 'pass', routeTo: 'none' }); }), 'was never measured there', 'verdict without a measurement');
|
|
172
|
+
await expectError(mutate((v) => { v.entries[0].results = [v.entries[0].results[0]]; }), 'and no verdict judges it', 'claim left unjudged');
|
|
173
|
+
await expectError(mutate((v) => { v.entries.push({ ...v.entries[0] }); }), 'is judged twice', 'one entry judged twice');
|
|
174
|
+
await expectError({ ...baseline(), 'response/response.json': responseJson({ fields: { 'frontend-surface-audit': 'response/response.md', verdicts: 'response/data/verdicts.json', capture: [CAP(WIDE), CAP(NARROW)], screenshot: [SHOT(WIDE)] } }) }, 'has verdicts and no screenshot', 'entry without a screenshot');
|
|
175
|
+
await expectError({ ...baseline(), 'response/response.json': responseJson({ next: ['quality.verify'] }) }, 'so next names frontend.presentation.resolve', 'failure that never reaches the resolver');
|
|
176
|
+
await expectError({ ...baseline(), [CAP(NARROW)]: { ...capture(NARROW), viewport: [1440, 900] } }, 'in the capture', 'receipt and capture disagree on the viewport');
|
|
177
|
+
await expectError({ ...baseline(), 'response/response.md': responseMd().replace('| 1rem | fail |', '| 1rem | pass |') }, 'is pass here and fail in the verdicts', 'receipt hides a failure');
|
|
178
|
+
const dropRegression = (text) => text.split(String.fromCharCode(10)).filter((l) => !l.startsWith('| `narrow-light-loaded` | `body>main`')).join(String.fromCharCode(10));
|
|
179
|
+
await expectError({ ...baseline(), 'response/response.md': dropRegression(responseMd()) }, 'Regressions has 0 rows, the verdicts carry 1 failures', 'regression dropped from the receipt');
|
|
180
|
+
await expectError({ ...baseline(), 'response/response.md': responseMd().replace('## Grammar gaps', '## Gaps') }, 'missing section ^## Grammar gaps$', 'receipt section renamed');
|
|
181
|
+
await expectError({ ...baseline(), 'response/data/verdicts.json': null, 'response/response.json': responseJson({ fields: { 'frontend-surface-audit': 'response/response.md', capture: [CAP(WIDE), CAP(NARROW)], screenshot: [SHOT(WIDE), SHOT(NARROW)] } }) }, 'required output verdicts is not in fields', 'missing required output');
|
|
182
|
+
await expectError({ ...baseline(), [CAP(WIDE)]: { ...capture(WIDE), viewport: [10, 10] } }, 'viewport', 'capture schema');
|
|
183
|
+
await expectError({ ...baseline(), 'response/response.json': responseJson({ status: 'blocked', stop: 'MADE_UP_CODE', next: [] }) }, 'not a registered code', 'unknown stop code');
|
|
184
|
+
|
|
185
|
+
process.stdout.write('frontend.surface.audit self-test: 2 valid branches, 20 rejected mutations\n');
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// frontend.surface.audit's own law over one branch, on top of the shared step check: every matrix entry in
|
|
2
|
+
// the verdicts has both a capture and a screenshot; every judged node was measured in that entry's
|
|
3
|
+
// capture and judged on a rule it actually claimed; a Grammar-owned node never routes a failure into
|
|
4
|
+
// the resolve loop and an application-owned failure always does; and the receipt reads what the
|
|
5
|
+
// verdicts carry.
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
7
|
+
import { readFile } from 'node:fs/promises';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import process from 'node:process';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { validateStep } from '../../scripts/validate-step.mjs';
|
|
12
|
+
import { tableUnder } from '../../scripts/validate-response.mjs';
|
|
13
|
+
|
|
14
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
15
|
+
const empty = (v) => v === undefined || v === null || v === '' || v === '—';
|
|
16
|
+
const list = (v) => (Array.isArray(v) ? v : empty(v) ? [] : [v]);
|
|
17
|
+
|
|
18
|
+
export async function validateAuditStep(branchDir, root = ROOT) {
|
|
19
|
+
const base = await validateStep(root, branchDir);
|
|
20
|
+
const errors = [...base.errors];
|
|
21
|
+
const { response, requirements = {}, present = new Set() } = base;
|
|
22
|
+
if (!response || response.operatorId !== 'frontend.surface.audit') return { errors };
|
|
23
|
+
const has = (f) => existsSync(path.join(branchDir, f));
|
|
24
|
+
const read = (f) => readFile(path.join(branchDir, f), 'utf8');
|
|
25
|
+
|
|
26
|
+
let verdicts = null;
|
|
27
|
+
if (present.has('verdicts') && has('response/data/verdicts.json')) {
|
|
28
|
+
try { verdicts = JSON.parse(await read('response/data/verdicts.json')); } catch { verdicts = null; }
|
|
29
|
+
}
|
|
30
|
+
if (!verdicts) {
|
|
31
|
+
if (response.status === 'done') errors.push('response/data/verdicts.json: a done branch needs the verdicts');
|
|
32
|
+
return { errors };
|
|
33
|
+
}
|
|
34
|
+
const at = 'response/data/verdicts.json';
|
|
35
|
+
|
|
36
|
+
const captureRefs = new Set(list(response.fields?.capture));
|
|
37
|
+
const shotRefs = new Set(list(response.fields?.screenshot));
|
|
38
|
+
const narrowed = list(requirements.matrix);
|
|
39
|
+
|
|
40
|
+
const captures = new Map();
|
|
41
|
+
for (const ref of captureRefs) {
|
|
42
|
+
if (!has(ref)) continue;
|
|
43
|
+
let doc = null;
|
|
44
|
+
try { doc = JSON.parse(await read(ref)); } catch { doc = null; }
|
|
45
|
+
if (doc) captures.set(doc.matrixId, { doc, ref });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const ids = new Set();
|
|
49
|
+
for (const entry of verdicts.entries) {
|
|
50
|
+
if (ids.has(entry.matrixId)) errors.push(`${at}: matrix entry ${entry.matrixId} is judged twice`);
|
|
51
|
+
ids.add(entry.matrixId);
|
|
52
|
+
const capture = captures.get(entry.matrixId);
|
|
53
|
+
if (!capture) { errors.push(`${at}: matrix entry ${entry.matrixId} has verdicts and no capture (EVIDENCE_MISSING)`); continue; }
|
|
54
|
+
if (capture.ref !== `response/data/captures/${entry.matrixId}.json`) errors.push(`${capture.ref}: the capture of ${entry.matrixId} is not the file its id names`);
|
|
55
|
+
if (!shotRefs.has(`response/artifacts/${entry.matrixId}.png`)) errors.push(`${at}: matrix entry ${entry.matrixId} has verdicts and no screenshot (EVIDENCE_MISSING)`);
|
|
56
|
+
if (narrowed.length && !narrowed.some((m) => (typeof m === 'string' ? m : m.matrixId) === entry.matrixId)) {
|
|
57
|
+
errors.push(`${at}: matrix entry ${entry.matrixId} was judged but the request narrowed the matrix without it`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const nodes = new Map(capture.doc.nodes.map((n) => [n.path, n]));
|
|
61
|
+
for (const result of entry.results) {
|
|
62
|
+
const node = nodes.get(result.path);
|
|
63
|
+
if (!node) { errors.push(`${at}: ${result.path} is judged under ${entry.matrixId} and was never measured there`); continue; }
|
|
64
|
+
if (node.owner !== result.owner) errors.push(`${at}: ${result.path} is ${result.owner} here and ${node.owner} in the capture`);
|
|
65
|
+
if (!node.claims.includes(result.rule)) errors.push(`${at}: ${result.path} is judged on ${result.rule}, which that node never claimed`);
|
|
66
|
+
const measured = Object.values(node.measured);
|
|
67
|
+
if (!measured.includes(result.measured)) errors.push(`${at}: ${result.path} is judged against ${result.measured}, which the capture did not measure`);
|
|
68
|
+
if (result.verdict === 'pass' && result.routeTo !== 'none') errors.push(`${at}: ${result.path} passes and still routes to ${result.routeTo}`);
|
|
69
|
+
if (result.verdict === 'fail') {
|
|
70
|
+
if (result.owner === 'grammar' && result.routeTo === 'resolve') errors.push(`${at}: ${result.path} is a Grammar component's own render; a failure there is a grammar-gap, never a resolve loop`);
|
|
71
|
+
if (result.owner === 'app' && result.routeTo !== 'resolve') errors.push(`${at}: ${result.path} is application-owned and its failure must route to resolve`);
|
|
72
|
+
if (result.routeTo === 'none') errors.push(`${at}: ${result.path} fails and routes nowhere`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Every claim a node carries is judged, or the claim is unaudited.
|
|
76
|
+
for (const node of capture.doc.nodes) {
|
|
77
|
+
for (const claim of node.claims) {
|
|
78
|
+
if (!entry.results.some((r) => r.path === node.path && r.rule === claim)) errors.push(`${at}: ${node.path} claims ${claim} under ${entry.matrixId} and no verdict judges it`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
for (const [matrixId] of captures) if (!ids.has(matrixId)) errors.push(`${at}: ${matrixId} was captured and never judged`);
|
|
83
|
+
|
|
84
|
+
const failing = verdicts.entries.flatMap((e) => e.results.filter((r) => r.verdict === 'fail'));
|
|
85
|
+
if (failing.some((r) => r.routeTo === 'resolve') && !(response.next ?? []).includes('frontend.presentation.resolve')) {
|
|
86
|
+
errors.push('response/response.json: a claim fails on an application-owned node, so next names frontend.presentation.resolve');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (present.has('frontend-surface-audit') && has('response/response.md')) {
|
|
90
|
+
const text = await read('response/response.md');
|
|
91
|
+
const rel = 'response/response.md';
|
|
92
|
+
const matrixRows = tableUnder(text, '## Matrix') ?? [];
|
|
93
|
+
if (matrixRows.length !== verdicts.entries.length) errors.push(`${rel}: Matrix has ${matrixRows.length} rows, the verdicts carry ${verdicts.entries.length} entries`);
|
|
94
|
+
for (const [id, viewport, scheme, state] of matrixRows) {
|
|
95
|
+
const capture = captures.get(id);
|
|
96
|
+
if (!capture) { errors.push(`${rel}: Matrix names ${id}, which has no capture`); continue; }
|
|
97
|
+
if (`${capture.doc.viewport[0]}x${capture.doc.viewport[1]}` !== viewport) errors.push(`${rel}: ${id} is ${viewport} here and ${capture.doc.viewport.join('x')} in the capture`);
|
|
98
|
+
if (capture.doc.scheme !== scheme || capture.doc.state !== state) errors.push(`${rel}: ${id} scheme or state differs from the capture`);
|
|
99
|
+
}
|
|
100
|
+
const rows = tableUnder(text, '## Verdicts by owner') ?? [];
|
|
101
|
+
const all = verdicts.entries.flatMap((e) => e.results);
|
|
102
|
+
if (rows.length !== all.length) errors.push(`${rel}: Verdicts by owner has ${rows.length} rows, the verdicts carry ${all.length} results`);
|
|
103
|
+
for (const [matrixId, owner, node, rule, , verdict] of rows) {
|
|
104
|
+
const entry = verdicts.entries.find((e) => e.matrixId === matrixId);
|
|
105
|
+
const result = entry?.results.find((r) => r.path === node && r.rule === rule);
|
|
106
|
+
if (!result) { errors.push(`${rel}: Verdicts by owner names ${rule} on ${node} under ${matrixId}, which the verdicts do not carry`); continue; }
|
|
107
|
+
if (result.owner !== owner) errors.push(`${rel}: ${node} is ${owner} here and ${result.owner} in the verdicts`);
|
|
108
|
+
if (result.verdict !== verdict) errors.push(`${rel}: ${rule} on ${node} is ${verdict} here and ${result.verdict} in the verdicts`);
|
|
109
|
+
}
|
|
110
|
+
const regressions = tableUnder(text, '## Regressions') ?? [];
|
|
111
|
+
if (regressions.length !== failing.length) errors.push(`${rel}: Regressions has ${regressions.length} rows, the verdicts carry ${failing.length} failures`);
|
|
112
|
+
for (const [matrixId, node, rule, , routeTo] of regressions) {
|
|
113
|
+
const entry = verdicts.entries.find((e) => e.matrixId === matrixId);
|
|
114
|
+
const result = entry?.results.find((r) => r.path === node && r.rule === rule && r.verdict === 'fail');
|
|
115
|
+
if (!result) { errors.push(`${rel}: Regressions names ${rule} on ${node}, which no verdict fails`); continue; }
|
|
116
|
+
if (result.routeTo !== routeTo) errors.push(`${rel}: ${rule} on ${node} routes to ${routeTo} here and ${result.routeTo} in the verdicts`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { errors };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
123
|
+
const target = process.argv[2];
|
|
124
|
+
if (!target) { process.stderr.write('usage: node validate.mjs <session>/step-N/parallel-M\n'); process.exit(2); }
|
|
125
|
+
const { errors } = await validateAuditStep(path.resolve(target));
|
|
126
|
+
if (errors.length) { process.stderr.write(`${errors.join('\n')}\n`); process.exitCode = 1; } else process.stdout.write('valid frontend.surface.audit branch\n');
|
|
127
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 9,
|
|
3
|
+
"note": "Stop codes only git.publish emits. Same entry shape as operators/errors.json, scope implicit. Shared codes (INVALID_INPUT, SOURCE_DRIFT, NO_PROGRESS) come from operators/errors.json; ROUTE_UNVERIFIED is defined by frontend.direction.decide and BRANCH_POLICY_VIOLATION is also emitted by workspace.bind; both belong in operators/errors.json with every emitting operator in scope.",
|
|
4
|
+
"codes": {
|
|
5
|
+
"APPROVAL_MISSING": {
|
|
6
|
+
"domain": "caller",
|
|
7
|
+
"disposition": "terminate",
|
|
8
|
+
"meaning": {
|
|
9
|
+
"en": "No approval covers this exact boundary unit; completion proof is not approval.",
|
|
10
|
+
"vi": "Không phê duyệt nào phủ đúng đơn vị ranh giới này; bằng chứng hoàn thành không phải phê duyệt."
|
|
11
|
+
},
|
|
12
|
+
"resume": {
|
|
13
|
+
"en": "Supply an approval issued for this unit.",
|
|
14
|
+
"vi": "Cấp một phê duyệt cấp cho đúng đơn vị này."
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"DIRTY_OUTSIDE_BOUNDARY": {
|
|
18
|
+
"domain": "source",
|
|
19
|
+
"disposition": "terminate",
|
|
20
|
+
"meaning": {
|
|
21
|
+
"en": "Something dirty lies outside the declared write roots, so the publish would carry work this boundary does not own.",
|
|
22
|
+
"vi": "Có thứ bẩn nằm ngoài các write root đã khai, nên lần publish sẽ mang theo công việc ranh giới này không sở hữu."
|
|
23
|
+
},
|
|
24
|
+
"resume": {
|
|
25
|
+
"en": "Clean the tree, or correct the write roots.",
|
|
26
|
+
"vi": "Dọn sạch cây làm việc, hoặc sửa lại các write root."
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"HOOK_BLOCKED": {
|
|
30
|
+
"domain": "source",
|
|
31
|
+
"disposition": "terminate",
|
|
32
|
+
"meaning": {
|
|
33
|
+
"en": "A Git hook rejected the publication, and no bypass is representable.",
|
|
34
|
+
"vi": "Một Git hook từ chối lần publish, và không có đường vòng nào biểu diễn được."
|
|
35
|
+
},
|
|
36
|
+
"resume": {
|
|
37
|
+
"en": "Fix the boundary and bring a new head.",
|
|
38
|
+
"vi": "Sửa ranh giới và mang một head mới."
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"NON_FAST_FORWARD": {
|
|
42
|
+
"domain": "remote",
|
|
43
|
+
"disposition": "terminate",
|
|
44
|
+
"meaning": {
|
|
45
|
+
"en": "The remote carries commits the local ref does not, so the push is not fast-forward.",
|
|
46
|
+
"vi": "Remote mang những commit mà ref cục bộ không có, nên cú push không phải fast-forward."
|
|
47
|
+
},
|
|
48
|
+
"resume": {
|
|
49
|
+
"en": "The branch owner reconciles the divergence and a new head arrives.",
|
|
50
|
+
"vi": "Người sở hữu nhánh hoà giải phần phân kỳ và một head mới tới."
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 9,
|
|
3
|
+
"id": "git.publish",
|
|
4
|
+
"domain": "git",
|
|
5
|
+
"job": "Publish one approved Git boundary from the exact commit quality verified, with non-force, fast-forward-only semantics, and stop with a typed failure rather than reaching for a bypass.",
|
|
6
|
+
"package": "operator.md",
|
|
7
|
+
"errors": "errors.json",
|
|
8
|
+
"validator": "validate.mjs",
|
|
9
|
+
"selfTest": "self-test.mjs",
|
|
10
|
+
"resources": {
|
|
11
|
+
"profile": "luna",
|
|
12
|
+
"grammarBound": false,
|
|
13
|
+
"tools": {
|
|
14
|
+
"@tools/fileread": "context-aliases",
|
|
15
|
+
"@tools/git": "merge-and-push",
|
|
16
|
+
"@tools/shell": "declared-commands",
|
|
17
|
+
"@tools/ci": "read"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# git.publish
|
|
2
|
+
|
|
3
|
+
## Job
|
|
4
|
+
|
|
5
|
+
Publish one approved Git boundary from the exact commit quality verified, with non-force,
|
|
6
|
+
fast-forward-only semantics, and stop with a typed failure rather than reaching for a bypass.
|
|
7
|
+
|
|
8
|
+
## It decides nothing about the change
|
|
9
|
+
|
|
10
|
+
Whether the work is correct was settled by the gates that produced the `quality-verification` receipt,
|
|
11
|
+
and whether it may be published was settled by the approval. This operator only performs the write, or
|
|
12
|
+
reports precisely why it did not. The receipt it leaves proves that exactly this commit reached
|
|
13
|
+
exactly this ref under exactly these hooks; it carries no verdict, no score, and no claim that
|
|
14
|
+
anything passed.
|
|
15
|
+
|
|
16
|
+
## The route is read, never rediscovered
|
|
17
|
+
|
|
18
|
+
This operator does not resolve a project to a checkout. `workspace.bind` does that, and its receipt
|
|
19
|
+
arrives here already bound. A publish that resolved its own path could publish from a checkout
|
|
20
|
+
nobody verified, which is the failure this separation exists to prevent. A route receipt whose
|
|
21
|
+
status is not `bound`, or which names another project, is `ROUTE_UNVERIFIED`.
|
|
22
|
+
|
|
23
|
+
## Approval is a person, always
|
|
24
|
+
|
|
25
|
+
Publishing pushes work out of the session and into a place other people pull from, so `approval` has
|
|
26
|
+
no default: an outward-facing act is always something a person said yes to. Completion proof records
|
|
27
|
+
the gates the boundary passed; it is evidence that the work is finished and never evidence that it
|
|
28
|
+
may be published. Exactly one approval must name this boundary. An approval issued for a different
|
|
29
|
+
boundary is a real approval for somebody else's work, which is exactly how unreviewed change rides
|
|
30
|
+
along with reviewed change, and it is `APPROVAL_MISSING`.
|
|
31
|
+
|
|
32
|
+
## The published commit is the verified commit
|
|
33
|
+
|
|
34
|
+
The `quality-verification` input measured one commit. That commit, and no other, is what this
|
|
35
|
+
publication pushes: a head one commit ahead of the verified one carries a change no gate ever saw.
|
|
36
|
+
The receipt records the verified commit beside the published head so the two can be compared later
|
|
37
|
+
without rerunning anything.
|
|
38
|
+
|
|
39
|
+
## Non-force is structural, not advisory
|
|
40
|
+
|
|
41
|
+
Force push, history rewrite, `reset --hard`, `clean`, `stash`, branch deletion and hook bypass are
|
|
42
|
+
not fields with a default; they are absent from this operator's vocabulary, so no request, in any
|
|
43
|
+
combination and under any justification, can ask for one. The reason is that each of them is most
|
|
44
|
+
tempting exactly when a publish has just failed: a rejected push, a red hook and an unexpected dirty
|
|
45
|
+
file each have an obvious one-command answer that destroys someone else's work or someone else's
|
|
46
|
+
evidence. `reset --hard` destroys uncommitted work no receipt has recorded, `clean` destroys
|
|
47
|
+
untracked files nobody has reviewed, and `stash` hides a dirty boundary instead of resolving it.
|
|
48
|
+
Making the request unrepresentable removes the decision from the moment it would be made badly. The
|
|
49
|
+
publication mode is always fast-forward only, and every publication records that it was not forced.
|
|
50
|
+
|
|
51
|
+
## The session branch is merged, never rebased
|
|
52
|
+
|
|
53
|
+
The producer did not write on the person's checked-out branch. It wrote on the session branch
|
|
54
|
+
`session/<sessionId>` of the routed checkout, in a git worktree prepared from the frozen head, and
|
|
55
|
+
committed its write set once. This operator merges that session branch into the target branch before
|
|
56
|
+
it pushes. When the target has not moved since the session base, the merge is a fast-forward and
|
|
57
|
+
nothing new is created. When the target has moved, a merge commit is allowed only under two
|
|
58
|
+
conditions together: the merge produced no conflict, and the gates the verification named were re-run
|
|
59
|
+
on the merge result and passed. A conflict is `NON_FAST_FORWARD`, it terminates, and a person
|
|
60
|
+
resolves it; the operator never rebases, never forces, and never runs with hooks disabled.
|
|
61
|
+
|
|
62
|
+
## A blocked hook is a result
|
|
63
|
+
|
|
64
|
+
Hooks are enforced, always, and `pre-push` is the last gate before the remote. A failing hook
|
|
65
|
+
produces `HOOK_BLOCKED` naming the hook, and the delta that clears it is a fixed boundary and a new
|
|
66
|
+
head. It is never a reason to run the push again with the hook disabled, to move the change onto a
|
|
67
|
+
branch whose hooks are lighter, or to commit the hook's own configuration out of the way. A
|
|
68
|
+
publication that carries a failed hook result, or that lacks the `pre-push` result altogether, is
|
|
69
|
+
refused.
|
|
70
|
+
|
|
71
|
+
## A rejected push is a result
|
|
72
|
+
|
|
73
|
+
When the remote carries commits the local ref does not, the push is not fast-forward. The operator
|
|
74
|
+
returns `NON_FAST_FORWARD` naming the remote head it observed. It does not rebase onto it, amend a
|
|
75
|
+
commit to make the push apply, squash the divergence away, force, or lease-force. Reconciling
|
|
76
|
+
divergent history changes what other people have already pulled, so it belongs to whoever owns the
|
|
77
|
+
branch, and that owner is not this operator.
|
|
78
|
+
|
|
79
|
+
## The boundary is exact
|
|
80
|
+
|
|
81
|
+
The boundary is the whole of what this publication owns, and the input `changes` names the paths
|
|
82
|
+
inside it. Anything dirty outside it is work this boundary does not own, and a publish that carries
|
|
83
|
+
it publishes somebody else's unreviewed change; that is `DIRTY_OUTSIDE_BOUNDARY`. Under a forbidden
|
|
84
|
+
worktree policy every published head is on the routed mutation branch, and a head on any other
|
|
85
|
+
branch is `BRANCH_POLICY_VIOLATION`. A publication that advances nothing is not a publication: the
|
|
86
|
+
published head is ahead of its upstream and the ref actually moved.
|
|
87
|
+
|
|
88
|
+
## The tag is asked for, or there is none
|
|
89
|
+
|
|
90
|
+
`tag` defaults to null, so a publication carries a continuation tag only when a person named one,
|
|
91
|
+
and that tag is annotated and points at a head this same publication pushed. A tag on a head this
|
|
92
|
+
run did not push is a label somebody else's commit now wears.
|
|
93
|
+
|
|
94
|
+
## Cleanup is part of the publish
|
|
95
|
+
|
|
96
|
+
After the push succeeds, the session worktree and the session branch are removed together with the
|
|
97
|
+
session folder, because the evidence they held has just become a published commit. A blocked session
|
|
98
|
+
keeps both: the evidence of what was attempted lives there until a person has read it.
|
|
99
|
+
|
|
100
|
+
## Boundary
|
|
101
|
+
|
|
102
|
+
Context is read-only apart from the merge and the push. The operator writes only `response/` of its
|
|
103
|
+
own branch, the target branch of the routed checkout, and the push to `@remote/git/<project>/<role>`:
|
|
104
|
+
the approved head on the routed ref, and at most one annotated continuation tag pointing at a head
|
|
105
|
+
this same publication pushed. It does not force push, lease-force push, or rewrite published history;
|
|
106
|
+
does not run reset, clean, or stash; does not delete a branch other than the session branch it is
|
|
107
|
+
cleaning up; does not bypass, skip, or disable a Git hook; does not amend, rebase, or squash a commit
|
|
108
|
+
to make a rejected push succeed; does not publish a head outside the approved boundary; and does not
|
|
109
|
+
publish without a verified route and an approval bound to this exact boundary.
|
|
110
|
+
|
|
111
|
+
## Context
|
|
112
|
+
|
|
113
|
+
| Alias | Bind | Required |
|
|
114
|
+
| --- | --- | --- |
|
|
115
|
+
| `@workspaces/local/routes/<project>/<role>` | the checkout, its session branch and its target branch, read at the frozen head | yes |
|
|
116
|
+
| `@workspaces/<project>/<role>/husky` | `pre-commit` and `pre-push`, which always run | yes |
|
|
117
|
+
| `@remote/git/<project>/<role>` | the publication target and the remote head observed at invocation time | yes |
|
|
118
|
+
|
|
119
|
+
## Inputs
|
|
120
|
+
|
|
121
|
+
| Kind | From | Required |
|
|
122
|
+
| --- | --- | --- |
|
|
123
|
+
| `workspace-route-binding` | `workspace.bind`; a publish never resolves its own checkout | yes |
|
|
124
|
+
| `changes` | `backend.source.apply` or `frontend.source.apply`, the exact file set this publication carries | yes |
|
|
125
|
+
| `quality-verification` | `quality.verify`, the receipt whose measured commit is the one this publication pushes | yes |
|
|
126
|
+
|
|
127
|
+
## Requirements
|
|
128
|
+
|
|
129
|
+
| Field | Type | Default | Ask |
|
|
130
|
+
| --- | --- | --- | --- |
|
|
131
|
+
| `boundary` | id | — | The one boundary being published, exactly as the approval names it |
|
|
132
|
+
| `approval` | id | — | The approval record that covers this boundary; completion is not approval |
|
|
133
|
+
| `tag` | `{name, message}` | null | One annotated continuation tag on the head this publication pushes, or none |
|
|
134
|
+
| `resume` | token | null | The blocked branch's token when re-entering after a stop |
|
|
135
|
+
|
|
136
|
+
## Steps
|
|
137
|
+
|
|
138
|
+
| # | Step | Params | Reads | Writes | Stops with |
|
|
139
|
+
| --- | --- | --- | --- | --- | --- |
|
|
140
|
+
| 1 | Validate the gate and resume | `resume` | `request/request.json`, @workspaces/local/routes/<project>/<role> at the frozen head, @remote/git/<project>/<role> as observed | — | `INVALID_INPUT`, `SOURCE_DRIFT`, `NO_PROGRESS` |
|
|
141
|
+
| 2 | Bind the route | — | input `workspace-route-binding`: the verified checkout, its head and its routed policy, and @workspaces/local/routes/<project>/<role> | — | `ROUTE_UNVERIFIED` |
|
|
142
|
+
| 3 | Bind the approval to this exact boundary | `boundary`, `approval` | `request/request.json` requirements, input `changes` as the file set, input `quality-verification` as the measured commit | — | `APPROVAL_MISSING` |
|
|
143
|
+
| 4 | Verify the tree: dirty outside the boundary, branch policy | — | @workspaces/local/routes/<project>/<role>, the dirty paths, every branch, the routed policy | — | `DIRTY_OUTSIDE_BOUNDARY`, `BRANCH_POLICY_VIOLATION` |
|
|
144
|
+
| 5 | Run the hooks | — | @workspaces/<project>/<role>/husky: the installed hooks, `pre-push` among them | @tools/shell | `HOOK_BLOCKED` |
|
|
145
|
+
| 6 | Merge the session branch into the target branch | — | @workspaces/local/routes/<project>/<role> for the target head, the session base and the session head | @workspaces/local/routes/<project>/<role>, the target branch of that checkout, @tools/git | `NON_FAST_FORWARD` |
|
|
146
|
+
| 7 | Push non-force, fast-forward only | — | @workspaces/local/routes/<project>/<role> for the approved head, @remote/git/<project>/<role> at the observed remote head, @tools/ci | @remote/git/<project>/<role>, @tools/git | `NON_FAST_FORWARD` |
|
|
147
|
+
| 8 | Push the continuation tag | `tag` | @workspaces/local/routes/<project>/<role> for the head this publication pushed | @remote/git/<project>/<role>, @tools/git | — |
|
|
148
|
+
| 9 | Remove the worktree and the session branch, write the receipt and emit | — | everything above | @workspaces/local/routes/<project>/<role>, `response/response.md`, `response/response.json`, @tools/git | — |
|
|
149
|
+
|
|
150
|
+
Creating a remote ref and fast-forwarding one are different acts with different reviewers, so the
|
|
151
|
+
published head records which of the two it performed. A resume begins again at validation, reuses
|
|
152
|
+
only unchanged fingerprinted observations, and consumes the exact delta; a resume that adds no head,
|
|
153
|
+
approval, hook or remote change is `NO_PROGRESS`, and a re-observed remote must arrive as a new
|
|
154
|
+
remote head because the same observation cannot yield a different result.
|
|
155
|
+
|
|
156
|
+
## Outputs
|
|
157
|
+
|
|
158
|
+
| Kind | File | Type | Required |
|
|
159
|
+
| --- | --- | --- | --- |
|
|
160
|
+
| `git-publication` | `response/response.md` | md | yes |
|
|
161
|
+
|
|
162
|
+
## Stops
|
|
163
|
+
|
|
164
|
+
| Code | Disposition |
|
|
165
|
+
| --- | --- |
|
|
166
|
+
| `INVALID_INPUT` | terminate |
|
|
167
|
+
| `SOURCE_DRIFT` | terminate |
|
|
168
|
+
| `NO_PROGRESS` | terminate |
|
|
169
|
+
| `ROUTE_UNVERIFIED` | terminate |
|
|
170
|
+
| `APPROVAL_MISSING` | terminate |
|
|
171
|
+
| `BRANCH_POLICY_VIOLATION` | terminate |
|
|
172
|
+
| `DIRTY_OUTSIDE_BOUNDARY` | terminate |
|
|
173
|
+
| `HOOK_BLOCKED` | terminate |
|
|
174
|
+
| `NON_FAST_FORWARD` | terminate |
|
|
175
|
+
|
|
176
|
+
## Next
|
|
177
|
+
|
|
178
|
+
| When | Operator |
|
|
179
|
+
| --- | --- |
|
|
180
|
+
| the boundary is published and the head must reach an environment | `release.deploy` |
|