@owlmeans/viable-common 0.0.24 → 0.0.26
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/README.md +4 -4
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/agent-presentation/SKILL.md +1 -1
- package/agent-meta/skills/viable-common/SKILL.md +115 -13
- package/build/blueprint/case.d.ts.map +1 -1
- package/build/blueprint/case.js +58 -16
- package/build/blueprint/case.js.map +1 -1
- package/build/blueprint/resolve.d.ts.map +1 -1
- package/build/blueprint/resolve.js +3 -0
- package/build/blueprint/resolve.js.map +1 -1
- package/build/blueprint/types.d.ts +44 -2
- package/build/blueprint/types.d.ts.map +1 -1
- package/build/blueprint/types.js +35 -1
- package/build/blueprint/types.js.map +1 -1
- package/build/branding.d.ts +5 -1
- package/build/branding.d.ts.map +1 -1
- package/build/branding.js +6 -0
- package/build/branding.js.map +1 -1
- package/build/connect/consts.d.ts +31 -43
- package/build/connect/consts.d.ts.map +1 -1
- package/build/connect/consts.js +34 -48
- package/build/connect/consts.js.map +1 -1
- package/build/connect/entrypoints.d.ts +36 -19
- package/build/connect/entrypoints.d.ts.map +1 -1
- package/build/connect/entrypoints.js +23 -4
- package/build/connect/entrypoints.js.map +1 -1
- package/build/connect/index.d.ts +0 -1
- package/build/connect/index.d.ts.map +1 -1
- package/build/connect/index.js +0 -1
- package/build/connect/index.js.map +1 -1
- package/build/connect/ops.d.ts +1 -4
- package/build/connect/ops.d.ts.map +1 -1
- package/build/connect/references.d.ts +33 -16
- package/build/connect/references.d.ts.map +1 -1
- package/build/connect/references.js +7 -1
- package/build/connect/references.js.map +1 -1
- package/build/connect/schemas.d.ts +17 -3
- package/build/connect/schemas.d.ts.map +1 -1
- package/build/connect/schemas.js +31 -5
- package/build/connect/schemas.js.map +1 -1
- package/build/connect/types.d.ts +63 -66
- package/build/connect/types.d.ts.map +1 -1
- package/build/consts.d.ts +1 -1
- package/build/consts.d.ts.map +1 -1
- package/build/consts.js +3 -0
- package/build/consts.js.map +1 -1
- package/build/design/runtime.d.ts +1 -1
- package/build/design/runtime.d.ts.map +1 -1
- package/build/metadata/types.d.ts +8 -0
- package/build/metadata/types.d.ts.map +1 -1
- package/build/metadata/types.js.map +1 -1
- package/build/planning/consts.d.ts +1 -1
- package/build/planning/consts.js +1 -1
- package/build/planning/helpers.d.ts +2 -1
- package/build/planning/helpers.d.ts.map +1 -1
- package/build/planning/helpers.js +5 -2
- package/build/planning/helpers.js.map +1 -1
- package/build/planning/index.d.ts +1 -0
- package/build/planning/index.d.ts.map +1 -1
- package/build/planning/index.js +1 -0
- package/build/planning/index.js.map +1 -1
- package/build/planning/landing.d.ts +25 -0
- package/build/planning/landing.d.ts.map +1 -0
- package/build/planning/landing.js +38 -0
- package/build/planning/landing.js.map +1 -0
- package/build/planning/schemas.d.ts.map +1 -1
- package/build/planning/schemas.js +13 -0
- package/build/planning/schemas.js.map +1 -1
- package/build/planning/types.d.ts +28 -3
- package/build/planning/types.d.ts.map +1 -1
- package/build/scaffold/consts.d.ts +8 -0
- package/build/scaffold/consts.d.ts.map +1 -1
- package/build/scaffold/consts.js +7 -0
- package/build/scaffold/consts.js.map +1 -1
- package/build/scaffold/schemas.d.ts.map +1 -1
- package/build/scaffold/schemas.js +257 -12
- package/build/scaffold/schemas.js.map +1 -1
- package/build/scaffold/types.d.ts +119 -4
- package/build/scaffold/types.d.ts.map +1 -1
- package/build/skills/catalogue.d.ts.map +1 -1
- package/build/skills/catalogue.js +150 -39
- package/build/skills/catalogue.js.map +1 -1
- package/build/skills/consts.d.ts +5 -0
- package/build/skills/consts.d.ts.map +1 -1
- package/build/skills/consts.js +8 -0
- package/build/skills/consts.js.map +1 -1
- package/build/skills/roles.d.ts.map +1 -1
- package/build/skills/roles.js +11 -5
- package/build/skills/roles.js.map +1 -1
- package/build/types.d.ts +30 -0
- package/build/types.d.ts.map +1 -1
- package/package.json +8 -8
- package/src/blueprint/case.ts +64 -16
- package/src/blueprint/resolve.ts +3 -0
- package/src/blueprint/types.ts +57 -1
- package/src/branding.ts +6 -0
- package/src/connect/consts.ts +32 -45
- package/src/connect/entrypoints.ts +51 -23
- package/src/connect/index.ts +0 -1
- package/src/connect/ops.ts +1 -4
- package/src/connect/references.ts +48 -29
- package/src/connect/schemas.ts +40 -11
- package/src/connect/types.ts +66 -69
- package/src/consts.ts +3 -0
- package/src/design/runtime.ts +1 -1
- package/src/metadata/types.ts +8 -0
- package/src/planning/consts.ts +1 -1
- package/src/planning/helpers.ts +5 -2
- package/src/planning/index.ts +1 -0
- package/src/planning/landing.ts +43 -0
- package/src/planning/schemas.ts +13 -0
- package/src/planning/types.ts +29 -3
- package/src/scaffold/consts.ts +10 -0
- package/src/scaffold/schemas.ts +262 -13
- package/src/scaffold/types.ts +105 -5
- package/src/skills/catalogue.ts +151 -39
- package/src/skills/consts.ts +8 -0
- package/src/skills/roles.ts +11 -5
- package/src/types.ts +30 -0
- package/tests/blueprint.spec.ts +98 -0
- package/tests/branding.spec.ts +21 -1
- package/tests/connect-convert.spec.ts +2 -8
- package/tests/connect-entrypoints.spec.ts +49 -28
- package/tests/planning.spec.ts +56 -1
- package/tests/scaffold.spec.ts +131 -0
- package/tests/skills.spec.ts +63 -2
- package/build/connect/jobs.d.ts +0 -25
- package/build/connect/jobs.d.ts.map +0 -1
- package/build/connect/jobs.js +0 -29
- package/build/connect/jobs.js.map +0 -1
- package/src/connect/jobs.ts +0 -40
package/src/skills/catalogue.ts
CHANGED
|
@@ -82,9 +82,6 @@ export const ALIAS_CONVENTION = `
|
|
|
82
82
|
\`web:layout:\` alias and no \`app.web.layout\` — writing either addresses nothing.
|
|
83
83
|
- An endpoint alias is \`api:<entity>:<action>\` — \`api:task:list\`, \`api:task:create\`. The
|
|
84
84
|
group alias that carries the shared path and guard is \`api:<entity>\` — \`api:task\`.
|
|
85
|
-
- A queued job alias is \`job:<entity>:<action>\` — \`job:report:build\`, \`job:contract:analyze\`.
|
|
86
|
-
A job is never given a \`web:\` or \`api:\` alias, and an endpoint is never given a \`job:\` one:
|
|
87
|
-
the prefix is what says which side serves it.
|
|
88
85
|
- Lowercase kebab-case in every segment. The prefix is NOT optional and NOT decorative: access
|
|
89
86
|
rules are applied per side by it.
|
|
90
87
|
`.trim()
|
|
@@ -103,11 +100,12 @@ The generated project is a \`@owlmeans/create-app\` monorepo with five workspace
|
|
|
103
100
|
- \`sources/web\` — the react application.
|
|
104
101
|
- \`sources/worker\` — the queue consumer. Same context, no HTTP routes, one processor per job.
|
|
105
102
|
|
|
106
|
-
These files are the CONTRACT between the packages. Every screen
|
|
107
|
-
them
|
|
103
|
+
These files are the CONTRACT between the public packages. Every screen and endpoint passes through
|
|
104
|
+
them; technical worker protocols live in the backend package instead:
|
|
108
105
|
|
|
109
106
|
- \`sources/common/src/consts.ts\` — the \`app\` alias tree.
|
|
110
107
|
- \`sources/common/src/entrypoints.ts\` — the shared immutable protocol tree (\`appEntrypoints\`).
|
|
108
|
+
- \`sources/backend/src/jobs/index.ts\` — backend-only queue protocols and declarations.
|
|
111
109
|
- \`sources/api/src/entrypoints.ts\` — server bindings: \`bind\` + one \`handlers<Context>()\` wrap
|
|
112
110
|
per handler.
|
|
113
111
|
- \`sources/web/src/entrypoints.ts\` — client bindings (\`bindAll\` + \`bindScreen\`).
|
|
@@ -598,39 +596,66 @@ carries. Introducing a second spelling orphans every grant made against the firs
|
|
|
598
596
|
files, not a copy of their own. They are already there: import them, never delete, rename or
|
|
599
597
|
move one, and never re-add a primitive the directory already carries.
|
|
600
598
|
- Styling is Tailwind utility classes. There are no CSS modules and no styled-components.
|
|
599
|
+
- The GROUND is white in light mode and black in dark mode, and it stays that way: every page,
|
|
600
|
+
section, band and header sits on \`bg-background\`. Colour comes from ONE product accent —
|
|
601
|
+
\`bg-primary\` / \`text-primary\` — and only on the primary button, a section's small label, the rule
|
|
602
|
+
over the first step and a check mark. It never comes from a background, a tint or a band.
|
|
603
|
+
|
|
604
|
+
<section className="bg-primary/10 py-20">...</section> // WRONG
|
|
605
|
+
<section className="bg-gradient-to-b from-amber-50 to-white py-20">...</section> // WRONG
|
|
606
|
+
<section className="border-t py-20">...</section> // right
|
|
607
|
+
|
|
608
|
+
- Depth comes from exactly three things: a 1px HAIRLINE (\`border\`, the \`--border\` token), the
|
|
609
|
+
NEUTRAL TILE (\`bg-muted\` — the only tinted surface: feature tiles, testimonial tiles, result
|
|
610
|
+
rows), and ONE shadow, \`shadow-floating\`, which belongs to the one card that floats (the landing
|
|
611
|
+
gate). An ordinary card is \`rounded-xl border bg-card\` and carries no shadow. There are no
|
|
612
|
+
gradients, no glass, no backdrop blur, no glow and no coloured shadows anywhere in the interface.
|
|
613
|
+
|
|
614
|
+
<div className="rounded-xl bg-white/60 shadow-2xl backdrop-blur-md p-6" /> // WRONG
|
|
615
|
+
<div className="rounded-xl border bg-card p-6" /> // right
|
|
616
|
+
<li className="rounded-[32px] bg-muted p-10" /> // right — a tile
|
|
617
|
+
|
|
618
|
+
- TYPE carries the brand. Headings are the theme's heavy grotesk — \`font-extrabold\` with tight
|
|
619
|
+
negative tracking (\`tracking-tight\`, or \`tracking-[-0.045em]\` on a section heading) and
|
|
620
|
+
\`text-balance\`; paragraphs are \`text-muted-foreground\` with \`text-pretty\` and a readable
|
|
621
|
+
measure (\`max-w-[52ch]\`); whitespace is generous. Never set a different font family.
|
|
622
|
+
- A section opens with a small LABEL — a plain \`<p className="text-[15px] font-bold text-primary">\`
|
|
623
|
+
— then its \`<h2>\`. A label is text: never a chip, a pill or a badge.
|
|
624
|
+
- Buttons are pills. The primary one is \`cta-btn\` (on an \`<a>\` or a bare \`<button>\`, never on a
|
|
625
|
+
shadcn \`Button\`, whose own \`bg-\` utility would win); the secondary one is
|
|
626
|
+
\`rounded-full border-[1.5px] border-foreground text-foreground hover:bg-muted\`. Labels are verbs.
|
|
627
|
+
- Motion is small and belongs to the picture: \`reveal\` fades a block in once as it enters, and
|
|
628
|
+
\`float-slow\` / \`drift-slow\` lift PARTS OF AN ILLUSTRATION by a few pixels. Nothing spins,
|
|
629
|
+
pulses, orbits or scales, and nothing moves for someone who asked for reduced motion.
|
|
630
|
+
- The stylesheet still defines the older classes \`gradient-heading\`, \`glass-card\`, \`eyebrow\`,
|
|
631
|
+
\`glow-orb\`, \`shadow-soft\`, \`shadow-raised\`, \`shadow-glow\`, \`pulse-soft\` and \`spin-slow\`,
|
|
632
|
+
because screens written earlier use them. They are REPAINTED flat — \`gradient-heading\` is solid
|
|
633
|
+
ink, \`glass-card\` a neutral tile, \`eyebrow\` a plain accent label, \`glow-orb\` hidden, the three
|
|
634
|
+
shadows and the two animations nothing at all. Never reach for one of them as decoration: it
|
|
635
|
+
paints nothing, or it paints the flat thing you could have written directly. Do not invent other
|
|
636
|
+
classes in the same family.
|
|
637
|
+
- Overrides: change any of the look rules above — the ground, the accent, depth, type, motion —
|
|
638
|
+
ONLY when the customer's specification explicitly asks: it names a background colour, a
|
|
639
|
+
dark-only or coloured theme, a brand palette, or gradients. The product's subject matter is not
|
|
640
|
+
a request — a bakery is not a warm background and a bank is not a blue one.
|
|
601
641
|
- Colour comes in PAIRS. Every surface token in this theme has a foreground partner —
|
|
602
642
|
\`--background\`/\`--foreground\`, \`--card\`/\`--card-foreground\`, \`--primary\`/\`--primary-foreground\`,
|
|
603
643
|
and the same for \`secondary\`, \`muted\`, \`accent\`, \`popover\`, \`destructive\` and \`sidebar\`. Painting a
|
|
604
644
|
surface is therefore never a single decision: give the element its partner in the SAME
|
|
605
645
|
\`className\`, or the text keeps the colour meant for the surface underneath and vanishes wherever
|
|
606
|
-
|
|
646
|
+
that surface is dark.
|
|
607
647
|
|
|
608
|
-
<
|
|
609
|
-
<
|
|
648
|
+
<a className="rounded-full bg-primary px-7 py-3">Get started</a> // WRONG
|
|
649
|
+
<a className="rounded-full bg-primary text-primary-foreground px-7 py-3">Get started</a> // right
|
|
610
650
|
|
|
611
|
-
Nothing catches the wrong form — it compiles and it renders. It is unreadable only
|
|
612
|
-
|
|
651
|
+
Nothing catches the wrong form — it compiles and it renders. It is unreadable only where the
|
|
652
|
+
accent happens to be dark, which is why it survives to the finished screen.
|
|
613
653
|
- Never paint a surface with a raw or palette colour — \`bg-slate-900\`, \`bg-[#101820]\`,
|
|
614
654
|
\`style={{ background: '#101820' }}\`. Those have no foreground partner, so nothing keeps the text
|
|
615
|
-
on them legible, and they ignore the theme. Every surface is one of the tokens
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
panel with depth), \`eyebrow\` (the small chip above a headline), \`cta-btn\` (the primary call to
|
|
620
|
-
action, on an \`<a>\` or a bare \`<button>\`, never on a shadcn \`Button\` whose own \`bg-\`
|
|
621
|
-
utility would win), \`glow-orb\` (a soft colour field behind a section, always
|
|
622
|
-
\`pointer-events-none\` and behind the content); the elevation utilities \`shadow-soft\`,
|
|
623
|
-
\`shadow-raised\`, \`shadow-floating\`, \`shadow-glow\`; the idle-motion classes \`reveal\`,
|
|
624
|
-
\`float-slow\`, \`drift-slow\`, \`pulse-soft\`, \`spin-slow\`; and gradients whose stops are THEME
|
|
625
|
-
tokens with opacity. None of these is a raw colour.
|
|
626
|
-
|
|
627
|
-
<div className="bg-[radial-gradient(#818cf8,transparent)] blur-[120px]" /> // WRONG
|
|
628
|
-
<span className="glow-orb h-80 w-96 -z-10" aria-hidden /> // right
|
|
629
|
-
<div className="bg-gradient-to-br from-primary/20 to-transparent p-6" /> // right
|
|
630
|
-
|
|
631
|
-
Keep a gradient under a text element light — \`/20\` or less — unless the element also carries a
|
|
632
|
-
\`-foreground\` partner class. These class names are defined in the project's stylesheet; do not
|
|
633
|
-
invent others in the same family.
|
|
655
|
+
on them legible, and they ignore the theme and its dark mode. Every surface is one of the tokens
|
|
656
|
+
above.
|
|
657
|
+
- Every interactive element keeps its visible \`focus-visible\` ring, is at least 44px tall to touch,
|
|
658
|
+
and never signals anything by hover alone.
|
|
634
659
|
- Text that is NOT on a coloured surface takes NO colour class: it already inherits the readable
|
|
635
660
|
one. Never add \`text-white\`, \`text-black\` or a \`-foreground\` class "to be safe" — on an ordinary
|
|
636
661
|
panel that is the same fault inverted. \`text-muted-foreground\` for secondary text is the
|
|
@@ -795,6 +820,84 @@ symbols from the component name (PascalCase of the component definition — for
|
|
|
795
820
|
Both sides must use these exact names. This is a DEFAULT: when a "View-model contract"
|
|
796
821
|
block is present in the task, it lists the symbols that actually exist and overrides this
|
|
797
822
|
convention entirely.
|
|
823
|
+
`),
|
|
824
|
+
|
|
825
|
+
skill(ViableSkill.LandingGate, 'The landing gate — from the landing page into the app', `
|
|
826
|
+
A product can let a guest START its key end-user workflow on the landing page, before any account
|
|
827
|
+
exists, and finish it in the app after signing in with the choices they made carried over. The
|
|
828
|
+
card that does this is the LANDING GATE. It sits in the hero where the call-to-action buttons
|
|
829
|
+
would be, and it REPLACES them.
|
|
830
|
+
|
|
831
|
+
**Which story is gate-worthy.** At most ONE per project, and only a story that is all of:
|
|
832
|
+
- in the \`user\` area — the signed-in END user's own workflow, never an operator's, an owner's or
|
|
833
|
+
one a guest already completes without signing in;
|
|
834
|
+
- a FIRST-VALUE step — something useful comes out of it the moment the guest acts;
|
|
835
|
+
- begun with NON-SENSITIVE input — a few picks, a filter, a short text. Never a payment, an
|
|
836
|
+
address, an upload, contact details or anything else about the person.
|
|
837
|
+
A pantry-to-recipe finder is gate-worthy: pick ingredients, see what you can bake. Checking out a
|
|
838
|
+
basket, setting up a profile or reviewing a queue of requests is not. When no story qualifies
|
|
839
|
+
there is no gate and the hero keeps its buttons — a correct answer, not a failure.
|
|
840
|
+
|
|
841
|
+
**The card.** The sketch is \`@/components/home/gate\`; the implemented one is
|
|
842
|
+
\`@/components/home/landing-gate\` and exports the same \`LandingGate\` with the same props.
|
|
843
|
+
- Its root is \`<div id="gate" data-home-gate>\` — never a \`<section>\`, \`<aside>\`, \`<header>\`
|
|
844
|
+
or \`<footer>\`; the page already has exactly one of each landmark it needs.
|
|
845
|
+
- A question in the end user's words as its heading, and a "No account needed" hint beside it.
|
|
846
|
+
- 5 to 8 choice chips, each a \`<button type="button" aria-pressed={selected}>\` inside a
|
|
847
|
+
\`role="group"\` with an \`aria-label\`, 4 or 5 pre-selected so the first view already shows
|
|
848
|
+
results. When choices do not fit, 1 to 3 labelled fields instead.
|
|
849
|
+
- A count with \`aria-live="polite"\`, at most 2 result rows re-ranked on every change, and one
|
|
850
|
+
line for when nothing matches. A row's lock icon is named "Full details after sign-in".
|
|
851
|
+
- A one-line note ("Sign in with your email. Your picks come with you.") and ONE primary button,
|
|
852
|
+
straight after the choices in tab order. No second button beside it.
|
|
853
|
+
- The sketch works signed out and offline from its sample records. The implemented card may call
|
|
854
|
+
a PUBLIC endpoint; never one that needs a signed-in user.
|
|
855
|
+
|
|
856
|
+
**The handoff.** The button carries the guest's choices across sign-in in \`sessionStorage\`,
|
|
857
|
+
through \`@/lib/handoff\` — never in the URL, never in a cookie, never through the server:
|
|
858
|
+
|
|
859
|
+
import { useLogin } from '@owlmeans/client-iam'
|
|
860
|
+
import { writeLandingHandoff } from '@/lib/handoff'
|
|
861
|
+
|
|
862
|
+
const [, login] = useLogin(target)
|
|
863
|
+
const start = () => {
|
|
864
|
+
writeLandingHandoff(target, { picks })
|
|
865
|
+
login()
|
|
866
|
+
}
|
|
867
|
+
<button type="button" className="cta-btn" onClick={start}>{cta}</button>
|
|
868
|
+
|
|
869
|
+
- \`target\` is the ALIAS of the story's own full-scale screen in the user area — the prop the guest
|
|
870
|
+
home passes — never a URL. \`useLogin(target)\` returns \`[path, handler]\`, and the handler
|
|
871
|
+
brings the person to \`target\` once they are signed in.
|
|
872
|
+
- The click handler is SYNCHRONOUS: write, then call the handler, in the same gesture. A sign-in
|
|
873
|
+
that has to open a window is eaten by the popup blocker once anything has been awaited first.
|
|
874
|
+
|
|
875
|
+
onClick={async () => { await saveDraft(picks); login() }} // WRONG — the gesture is gone
|
|
876
|
+
href={'/frontoffice/recipes?picks=' + picks.join(',')} // WRONG — a URL, the picks in it, no sign-in
|
|
877
|
+
|
|
878
|
+
**The full-scale screen** — the story's own screen in the user area — reads the handoff once in its
|
|
879
|
+
view model, pre-fills from it, says so in one line, and clears it:
|
|
880
|
+
|
|
881
|
+
import { clearLandingHandoff, readLandingHandoff } from '@/lib/handoff'
|
|
882
|
+
|
|
883
|
+
useEffect(() => {
|
|
884
|
+
const carried = readLandingHandoff<{ picks: string[] }>(alias)
|
|
885
|
+
if (carried == null) return
|
|
886
|
+
setPicks(carried.picks)
|
|
887
|
+
setCarriedOver(true) // "We kept your 4 picks from the home page."
|
|
888
|
+
clearLandingHandoff()
|
|
889
|
+
}, [alias])
|
|
890
|
+
|
|
891
|
+
\`alias\` is the screen's own alias — the same string the gate was given as \`target\`. A missing,
|
|
892
|
+
expired or foreign handoff reads as \`null\`, and the screen then starts empty exactly as it does for
|
|
893
|
+
someone who arrived from the menu; never treat its absence as an error.
|
|
894
|
+
|
|
895
|
+
**Never:**
|
|
896
|
+
- ask for an account, a payment, an email address or any personal detail before sign-in — the gate
|
|
897
|
+
collects choices and nothing else, and the handoff stores only those choices;
|
|
898
|
+
- render a story code, a "Preview" badge or a dashed frame inside the gate — it is the product's
|
|
899
|
+
public face in production;
|
|
900
|
+
- keep the hero's buttons beside a gate, or give the gate a second call to action.
|
|
798
901
|
`),
|
|
799
902
|
|
|
800
903
|
skill(ViableSkill.ResourceLayer, 'OwlMeans resources own the schema', `
|
|
@@ -1390,25 +1493,34 @@ the work is invisible and the user is left pressing a button that appears to do
|
|
|
1390
1493
|
`),
|
|
1391
1494
|
|
|
1392
1495
|
skill(ViableSkill.WorkerJobs, 'Queues, jobs and processors', `
|
|
1393
|
-
Three
|
|
1496
|
+
Three server-side bindings have to agree, and a job that exists in two of the three is worse than one that
|
|
1394
1497
|
exists in none — a declared name nothing processes is a message that piles up, and a processor
|
|
1395
1498
|
with no declaration is dead code the barrel still imports.
|
|
1396
1499
|
|
|
1397
|
-
## 1. The queue — \`sources/backend/src/jobs/index.ts\`
|
|
1500
|
+
## 1. The protocol and queue — \`sources/backend/src/jobs/index.ts\`
|
|
1398
1501
|
A queue is an ADDRESS: it says what exists and which job names it accepts. Both the api (which
|
|
1399
1502
|
enqueues) and the worker (which consumes) read this one list, and a job name the queue does not
|
|
1400
|
-
declare is refused at enqueue time.
|
|
1503
|
+
declare is refused at enqueue time. The technical protocol belongs here too; common and web must
|
|
1504
|
+
not import it.
|
|
1401
1505
|
|
|
1402
1506
|
\`\`\`ts
|
|
1507
|
+
export const jobProtocols = {
|
|
1508
|
+
reportBuild: protocol(route(
|
|
1509
|
+
'job:report:build', '/report-build',
|
|
1510
|
+
queueRoute({ service: APP_WORKER, queue: APP_QUEUE, timeout: 30_000 }),
|
|
1511
|
+
), contract(typed())),
|
|
1512
|
+
}
|
|
1403
1513
|
export const queues: QueueDeclaration[] = [
|
|
1404
|
-
{ name: APP_QUEUE, jobs: [
|
|
1514
|
+
{ name: APP_QUEUE, jobs: [jobProtocols.reportBuild.alias],
|
|
1405
1515
|
worker: { concurrency: 4, lockDuration: 60_000 } },
|
|
1406
1516
|
]
|
|
1407
1517
|
\`\`\`
|
|
1408
1518
|
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1519
|
+
Use \`job()\` from \`@owlmeans/queue\`, never from the generic route package.
|
|
1520
|
+
|
|
1521
|
+
## 2. The producer — \`sources/api/src/entrypoints.ts\`
|
|
1522
|
+
Bind \`bindClient(jobProtocols.reportBuild)\`. Public API contracts in common describe the domain
|
|
1523
|
+
operation and do not disclose that the handler delegates to a queue.
|
|
1412
1524
|
|
|
1413
1525
|
## 3. The processor — \`sources/worker/src/jobs/<name>.ts\`
|
|
1414
1526
|
A PLAIN exported async function — named exports only, exactly like an endpoint handler. It never
|
|
@@ -1421,7 +1533,7 @@ imports \`handlers\` and never calls \`handlers<Context>()\`; that belongs only
|
|
|
1421
1533
|
// sources/worker/src/entrypoints.ts — the ONLY wrap
|
|
1422
1534
|
import * as jobs from '@/jobs/index.js'
|
|
1423
1535
|
const worker = handlers<Context>()
|
|
1424
|
-
bind(
|
|
1536
|
+
bind(jobProtocols.reportBuild, worker.request(jobProtocols.reportBuild, jobs.handleReportBuildJob))
|
|
1425
1537
|
|
|
1426
1538
|
WRONG — the processor module ALSO calls \`handlers()\` and wraps itself, so the export bound above
|
|
1427
1539
|
is wrapped a SECOND time. \`tsc\` catches it (\`TS2345 "BoundEntrypointHandler<…> is not
|
|
@@ -1430,7 +1542,7 @@ function\`:
|
|
|
1430
1542
|
|
|
1431
1543
|
// WRONG — sources/worker/src/jobs/report-build.ts
|
|
1432
1544
|
const worker = handlers<Context>()
|
|
1433
|
-
export const handleReportBuildJob = worker.request(
|
|
1545
|
+
export const handleReportBuildJob = worker.request(jobProtocols.reportBuild, async (req, ctx) => { ... })
|
|
1434
1546
|
|
|
1435
1547
|
It RETURNS its result; throwing a \`ResilientError\` subclass is how a refusal is reported, and the
|
|
1436
1548
|
class survives the broker.
|
|
@@ -1443,7 +1555,7 @@ Two rules with no equivalent on the HTTP side:
|
|
|
1443
1555
|
what it created, and say in a comment which of the two this one does.
|
|
1444
1556
|
|
|
1445
1557
|
Enqueue from an endpoint with the same typed call used for HTTP:
|
|
1446
|
-
\`context.entrypoint(
|
|
1558
|
+
\`context.entrypoint(jobProtocols.<name>).call({ body: data })\`.
|
|
1447
1559
|
`),
|
|
1448
1560
|
|
|
1449
1561
|
skill(ViableSkill.TargetAgents, 'LLM agents inside the application', `
|
package/src/skills/consts.ts
CHANGED
|
@@ -31,6 +31,11 @@ export enum ViableSkill {
|
|
|
31
31
|
FormFeedback = 'form-feedback',
|
|
32
32
|
StoreAccess = 'store-access',
|
|
33
33
|
ViewModelNaming = 'viewmodel-naming',
|
|
34
|
+
/**
|
|
35
|
+
* The landing gate: a guest starts the key end-user story on the landing page and continues on
|
|
36
|
+
* its full-scale screen after signing in, with the choices carried over.
|
|
37
|
+
*/
|
|
38
|
+
LandingGate = 'landing-gate',
|
|
34
39
|
|
|
35
40
|
ResourceLayer = 'resource-layer',
|
|
36
41
|
ResourceResults = 'resource-results',
|
|
@@ -99,6 +104,9 @@ export const SKILL_ORDER: Record<ViableSkill, number> = {
|
|
|
99
104
|
[ViableSkill.FormFeedback]: 51,
|
|
100
105
|
[ViableSkill.StoreAccess]: 52,
|
|
101
106
|
[ViableSkill.ViewModelNaming]: 55,
|
|
107
|
+
// After the frontend rules it builds on (entrypoints, nav, shadcn, form feedback, the store), and
|
|
108
|
+
// before the backend block — it is only ever carried by analysis and frontend personas.
|
|
109
|
+
[ViableSkill.LandingGate]: 56,
|
|
102
110
|
[ViableSkill.ResourceLayer]: 60,
|
|
103
111
|
[ViableSkill.ResourceResults]: 62,
|
|
104
112
|
[ViableSkill.ResourceMigrations]: 64,
|
package/src/skills/roles.ts
CHANGED
|
@@ -111,11 +111,17 @@ and you are told which one with each task.
|
|
|
111
111
|
|
|
112
112
|
[ViablePersona.VisualDesigner]: persona(`
|
|
113
113
|
You are a visual ui designer specializing in writing design guidelines and design systems
|
|
114
|
-
for web applications
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
114
|
+
for web applications, and your interfaces are calm, precise and unmistakably the product's own.
|
|
115
|
+
Your house style: a white ground in light mode and a black one in dark mode; colour from ONE
|
|
116
|
+
product accent and never from backgrounds; depth only from hairlines, neutral tiles and a single
|
|
117
|
+
floating shadow; a heavy grotesk display face with tight tracking and generous whitespace carrying
|
|
118
|
+
the brand; motion that is small and belongs to the picture. You never reach for gradients, glass,
|
|
119
|
+
blur or glow, and the older decoration classes a project still defines are painted flat and are
|
|
120
|
+
never your tools. You change any of this only when the customer's specification explicitly asks —
|
|
121
|
+
it names a background colour, a dark-only or coloured theme, a brand palette, or gradients. The
|
|
122
|
+
product's subject matter is not such a request: a bakery is not a warm background, a bank is not a
|
|
123
|
+
blue one. What makes a design this product's is its accent, its type and its illustration — never
|
|
124
|
+
an added effect.
|
|
119
125
|
This application serves four audiences — anonymous visitors, signed-in end users, the owner,
|
|
120
126
|
and staff running the business process. Every screen you style belongs to exactly one of them,
|
|
121
127
|
and you are told which one with each task.
|
package/src/types.ts
CHANGED
|
@@ -33,6 +33,17 @@ export interface SlotConstMetadata {
|
|
|
33
33
|
brandingPrivacyUrl: string
|
|
34
34
|
brandingCredit: string
|
|
35
35
|
brandingHideCreditIntent: string
|
|
36
|
+
/**
|
|
37
|
+
* The Google tag the owner attached — a `GTM-` container or a `G-`/`GT-`/`AW-`/`DC-` tag id —
|
|
38
|
+
* loaded by the generated application behind its cookie consent, in the preview and in
|
|
39
|
+
* production alike.
|
|
40
|
+
*
|
|
41
|
+
* OPTIONAL, unlike the branding keys above, and OMITTED from a push while it is unset: a slot
|
|
42
|
+
* publisher built before the key existed validates the signed body against a schema that does
|
|
43
|
+
* not declare it, strips it, and refuses the whole configuration with a 401. An unset tag must
|
|
44
|
+
* therefore never reach one — which only absence guarantees.
|
|
45
|
+
*/
|
|
46
|
+
brandingGoogleTag?: string
|
|
36
47
|
}
|
|
37
48
|
|
|
38
49
|
export interface SlotSecretMetadata {
|
|
@@ -82,6 +93,25 @@ export interface SlotListMetadata {
|
|
|
82
93
|
* hosts are always accepted and are never in this list.
|
|
83
94
|
*/
|
|
84
95
|
allowedOrigins: string[]
|
|
96
|
+
/**
|
|
97
|
+
* Extra Content-Security-Policy source expressions the slot's runtime adds to what it serves —
|
|
98
|
+
* a third party a generated app legitimately needs that the platform does not already know about.
|
|
99
|
+
*
|
|
100
|
+
* OPTIONAL and omitted from a push while empty, for the reason `brandingGoogleTag` is: an older
|
|
101
|
+
* publisher refuses a signed body carrying a key its schema does not declare, and a slot
|
|
102
|
+
* provisioned before the key existed simply has no value for it. The runtime reads it
|
|
103
|
+
* defensively and filters every source before it reaches a header.
|
|
104
|
+
*
|
|
105
|
+
* The Google tag's hosts are NOT carried here: the publisher derives them itself from
|
|
106
|
+
* `brandingGoogleTag` (and, for a production build, from the record the build writes), so the
|
|
107
|
+
* tag and the hosts it needs cannot disagree. Nothing writes this key today; it is declared so the
|
|
108
|
+
* publisher's existing reader has a vocabulary entry and a schema slot.
|
|
109
|
+
*
|
|
110
|
+
* Deliberately NOT in `metadataLists`. That array enumerates the lists an OWNER stores — one
|
|
111
|
+
* `project-config` row each, loaded on every metadata read and pushed back verbatim — and no
|
|
112
|
+
* owner-facing setting stores this one.
|
|
113
|
+
*/
|
|
114
|
+
cspSources?: string[]
|
|
85
115
|
}
|
|
86
116
|
|
|
87
117
|
export interface CustomSecretMetadata {
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
applyBlueprintCase, BLUEPRINT_CASES, BlueprintCase, landingGatePreferenceOf, LandingGatePreference,
|
|
4
|
+
personaSkillsOf,
|
|
5
|
+
} from '../src/blueprint/index.js'
|
|
6
|
+
import type { Blueprint } from '../src/blueprint/index.js'
|
|
7
|
+
import { ViableSkill } from '../src/skills/consts.js'
|
|
8
|
+
import { ViablePersona } from '../src/skills/roles.js'
|
|
9
|
+
|
|
10
|
+
/** Just enough of a blueprint for a case to be applied to — the shape a test builds. */
|
|
11
|
+
const base = (extra: Partial<Blueprint> = {}): Blueprint => ({
|
|
12
|
+
id: 'test-blueprint',
|
|
13
|
+
title: 'Test',
|
|
14
|
+
technology: { id: 'ts', language: 'typescript', runtime: 'bun', extensions: ['.ts'], buildScripts: {}, packageManager: 'bun' },
|
|
15
|
+
stack: { id: 'owlmeans', framework: 'owlmeans', topology: {} as Blueprint['stack']['topology'], skills: [ViableSkill.TsStyle] },
|
|
16
|
+
template: { id: 'seed', overlay: 'seed', patches: [] },
|
|
17
|
+
createApp: { id: 'create-app', package: '@owlmeans/create-app', bare: true, template: 'bare' },
|
|
18
|
+
packages: {
|
|
19
|
+
id: 'packages',
|
|
20
|
+
capabilities: { postgres: true, kv: false, queue: false, worker: false, agents: false, validation: true },
|
|
21
|
+
libraries: {},
|
|
22
|
+
skills: [ViableSkill.ShadcnUi],
|
|
23
|
+
personaSkills: { [ViablePersona.BusinessAnalyst]: [ViableSkill.MainFlowFocus] },
|
|
24
|
+
},
|
|
25
|
+
...extra,
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
describe('viable-common - the landing-gate preference', () => {
|
|
29
|
+
test('answers Allow for no blueprint, no layer and a value this deploy never heard of', () => {
|
|
30
|
+
expect(landingGatePreferenceOf()).toBe(LandingGatePreference.Allow)
|
|
31
|
+
expect(landingGatePreferenceOf(base())).toBe(LandingGatePreference.Allow)
|
|
32
|
+
expect(landingGatePreferenceOf({ experience: { landingGate: 'insist' as never } }))
|
|
33
|
+
.toBe(LandingGatePreference.Allow)
|
|
34
|
+
expect(landingGatePreferenceOf({ experience: { landingGate: LandingGatePreference.Discourage } }))
|
|
35
|
+
.toBe(LandingGatePreference.Discourage)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('each case weighs the gate: web and the AI kinds encourage, scalable allows, a game discourages', () => {
|
|
39
|
+
const preferences = Object.fromEntries(Object.values(BlueprintCase).map(id =>
|
|
40
|
+
[id, landingGatePreferenceOf(applyBlueprintCase(base(), id))]))
|
|
41
|
+
|
|
42
|
+
expect(preferences).toEqual({
|
|
43
|
+
[BlueprintCase.Web]: LandingGatePreference.Encourage,
|
|
44
|
+
[BlueprintCase.Scalable]: LandingGatePreference.Allow,
|
|
45
|
+
[BlueprintCase.AiPipeline]: LandingGatePreference.Encourage,
|
|
46
|
+
[BlueprintCase.AiAgent]: LandingGatePreference.Encourage,
|
|
47
|
+
[BlueprintCase.Game]: LandingGatePreference.Discourage,
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('a case overrides the base layer key by key, and no case leaves a blueprint without one', () => {
|
|
52
|
+
const allowing = base({ experience: { landingGate: LandingGatePreference.Allow } })
|
|
53
|
+
|
|
54
|
+
expect(landingGatePreferenceOf(applyBlueprintCase(allowing, BlueprintCase.Game)))
|
|
55
|
+
.toBe(LandingGatePreference.Discourage)
|
|
56
|
+
expect(applyBlueprintCase(base(), undefined).experience).toBeUndefined()
|
|
57
|
+
expect(Object.isFrozen(applyBlueprintCase(base(), BlueprintCase.Web).experience)).toBe(true)
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
describe('viable-common - the landing-gate skill in the cases', () => {
|
|
62
|
+
const gatePersonas = [
|
|
63
|
+
ViablePersona.BusinessAnalyst, ViablePersona.UxDesigner, ViablePersona.LayoutArchitect,
|
|
64
|
+
ViablePersona.ComponentArchitect, ViablePersona.UiViewCoder, ViablePersona.UiNavCoder,
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
test('reaches the analyst and the frontend personas of every encouraging case, and nobody else', () => {
|
|
68
|
+
for (const id of [BlueprintCase.Web, BlueprintCase.AiPipeline, BlueprintCase.AiAgent]) {
|
|
69
|
+
const cased = applyBlueprintCase(base(), id)
|
|
70
|
+
for (const persona of gatePersonas) {
|
|
71
|
+
expect(personaSkillsOf(cased, persona), `${id} → ${persona}`).toContain(ViableSkill.LandingGate)
|
|
72
|
+
}
|
|
73
|
+
expect(personaSkillsOf(cased, ViablePersona.ApiCoder)).not.toContain(ViableSkill.LandingGate)
|
|
74
|
+
}
|
|
75
|
+
for (const id of [BlueprintCase.Scalable, BlueprintCase.Game]) {
|
|
76
|
+
const cased = applyBlueprintCase(base(), id)
|
|
77
|
+
for (const persona of gatePersonas) {
|
|
78
|
+
expect(personaSkillsOf(cased, persona), `${id} → ${persona}`).not.toContain(ViableSkill.LandingGate)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('is merged with what a case and the base already give a persona, never in place of it', () => {
|
|
84
|
+
const agent = applyBlueprintCase(base(), BlueprintCase.AiAgent)
|
|
85
|
+
|
|
86
|
+
expect(personaSkillsOf(agent, ViablePersona.BusinessAnalyst)).toEqual([
|
|
87
|
+
ViableSkill.MainFlowFocus, ViableSkill.AgenticChoice, ViableSkill.LandingGate,
|
|
88
|
+
])
|
|
89
|
+
expect(personaSkillsOf(agent, ViablePersona.UxDesigner))
|
|
90
|
+
.toEqual([ViableSkill.AgenticChoice, ViableSkill.LandingGate])
|
|
91
|
+
expect(personaSkillsOf(agent, ViablePersona.WorkerCoder)).toEqual([
|
|
92
|
+
ViableSkill.AgenticChoice, ViableSkill.TargetLlm, ViableSkill.LibrariesAi,
|
|
93
|
+
ViableSkill.TargetAgentTools,
|
|
94
|
+
])
|
|
95
|
+
expect(BLUEPRINT_CASES[BlueprintCase.AiPipeline].patch.packages?.personaSkills?.[ViablePersona.BusinessAnalyst])
|
|
96
|
+
.toEqual([ViableSkill.AgenticChoice, ViableSkill.LandingGate])
|
|
97
|
+
})
|
|
98
|
+
})
|
package/tests/branding.spec.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from 'bun:test'
|
|
2
2
|
|
|
3
|
-
import { brandingEnv } from '../src/branding.js'
|
|
3
|
+
import { BRANDING_ENV_KEYS, brandingEnv } from '../src/branding.js'
|
|
4
|
+
import { metadataConfigs, metadataLists } from '../src/consts.js'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* `BRANDING_CREDIT` is the one key whose ABSENCE means something different from an explicit
|
|
@@ -32,6 +33,25 @@ describe('brandingEnv', () => {
|
|
|
32
33
|
BRANDING_PRIVACY_URL: '',
|
|
33
34
|
BRANDING_CREDIT: '1',
|
|
34
35
|
BRANDING_PRODUCT: 'Acme CRM',
|
|
36
|
+
BRANDING_GOOGLE_TAG: '',
|
|
35
37
|
})
|
|
36
38
|
})
|
|
39
|
+
|
|
40
|
+
it('carries the Google tag, and emits it empty when none is set', () => {
|
|
41
|
+
// The METADATA key is omitted from a push while unset (an older publisher refuses it); the
|
|
42
|
+
// ENVIRONMENT key is always there, and '' is what tells the build to load no tag.
|
|
43
|
+
expect(brandingEnv({ brandingGoogleTag: 'G-ABC123XYZ' }).BRANDING_GOOGLE_TAG).toBe('G-ABC123XYZ')
|
|
44
|
+
expect(brandingEnv({}).BRANDING_GOOGLE_TAG).toBe('')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('emits exactly the keys BRANDING_ENV_KEYS names', () => {
|
|
48
|
+
expect(Object.keys(brandingEnv({})).sort()).toEqual([...BRANDING_ENV_KEYS].sort())
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe('the metadata vocabulary', () => {
|
|
53
|
+
it('delivers the Google tag as a config key, and keeps the derived CSP list out of the stored lists', () => {
|
|
54
|
+
expect(metadataConfigs).toContain('brandingGoogleTag')
|
|
55
|
+
expect(metadataLists as string[]).not.toContain('cspSources')
|
|
56
|
+
})
|
|
37
57
|
})
|
|
@@ -3,7 +3,7 @@ import Ajv from 'ajv'
|
|
|
3
3
|
import addFormats from 'ajv-formats'
|
|
4
4
|
import { RouteMethod, route } from '@owlmeans/route'
|
|
5
5
|
import { aliasOf, openProtocol, protocols } from '@owlmeans/entrypoint'
|
|
6
|
-
import { connect, ConnectInquiryKind,
|
|
6
|
+
import { connect, ConnectInquiryKind, ConnectOpKind } from '../src/connect/consts.js'
|
|
7
7
|
import { connectProtocols } from '../src/connect/entrypoints.js'
|
|
8
8
|
import { connectRef } from '../src/connect/references.js'
|
|
9
9
|
import {
|
|
@@ -151,12 +151,6 @@ describe('viable-common - the conversion additions to the connector contract', (
|
|
|
151
151
|
// while the two enums stay byte-identical — `manager-api` does not depend on the model
|
|
152
152
|
// runtime, so nothing else in the stack would notice one of them gaining a fourth shape.
|
|
153
153
|
expect(Object.values(ConnectInquiryKind)).toEqual(Object.values(InquiryKind))
|
|
154
|
-
|
|
155
|
-
// names, so a single `convert` kind would match whichever run row happened to be newest.
|
|
156
|
-
expect(ConnectJobKind.ConvertIntake).toBe('convert-intake')
|
|
157
|
-
expect(ConnectJobKind.ConvertAnalysis).toBe('convert-analysis')
|
|
158
|
-
expect(ConnectJobKind.ConvertExtraction).toBe('convert-extraction')
|
|
159
|
-
expect(ConnectJobKind.ConvertImplementation).toBe('convert-implementation')
|
|
160
|
-
expect(ConnectJobKind.ConvertPurge).toBe('convert-purge')
|
|
154
|
+
expect(connect.convert.status).toBe('viable:manager-api:connect:convert:status')
|
|
161
155
|
})
|
|
162
156
|
})
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import
|
|
2
|
+
import Ajv from 'ajv'
|
|
3
|
+
import { aliasOf, openProtocol, protocols } from '@owlmeans/entrypoint'
|
|
3
4
|
import { DEFAULT_GUARD } from '@owlmeans/auth-common'
|
|
4
|
-
import { route } from '@owlmeans/route'
|
|
5
|
-
import { connect,
|
|
5
|
+
import { route, RouteMethod } from '@owlmeans/route'
|
|
6
|
+
import { connect, CONNECT_BRANDING_GOOGLE_TAG_MAX } from '../src/connect/consts.js'
|
|
7
|
+
import { ConnectProjectBrandingSaveSchema } from '../src/connect/schemas.js'
|
|
6
8
|
import { connectProtocols } from '../src/connect/entrypoints.js'
|
|
7
|
-
import { JOB_SEPARATOR, jobIdOf, parseJobId } from '../src/connect/jobs.js'
|
|
8
9
|
import { connectRef } from '../src/connect/references.js'
|
|
9
10
|
|
|
10
11
|
describe('@owlmeans/viable-common — connector protocol tree', () => {
|
|
@@ -18,40 +19,60 @@ describe('@owlmeans/viable-common — connector protocol tree', () => {
|
|
|
18
19
|
expect(tree.session.open.alias).toBe(connect.session.open)
|
|
19
20
|
expect(tree.project.llm.alias).toBe(connect.project.llm)
|
|
20
21
|
expect(tree.project.converterLlm.alias).toBe(connect.project.converterLlm)
|
|
22
|
+
expect(tree.story.status.alias).toBe(connect.story.status)
|
|
21
23
|
expect(tree.convert.proceed.alias).toBe(connect.convert.proceed)
|
|
22
24
|
expect(tree.inquiry.answer.alias).toBe(connect.inquiry.answer)
|
|
23
25
|
expect(tree.files.list.alias).toBe(connect.files.list)
|
|
24
|
-
expect(protocols(tree)).toHaveLength(
|
|
25
|
-
expect(new Set(protocols(tree).map(protocol => protocol.alias)).size).toBe(
|
|
26
|
+
expect(protocols(tree)).toHaveLength(34)
|
|
27
|
+
expect(new Set(protocols(tree).map(protocol => protocol.alias)).size).toBe(34)
|
|
26
28
|
})
|
|
27
29
|
|
|
28
|
-
test('
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
test('reads and saves a project\'s branding at one path, under the owned base and no paid gate', () => {
|
|
31
|
+
const paid = connectProtocols({
|
|
32
|
+
guard: DEFAULT_GUARD,
|
|
33
|
+
gate: { alias: 'test-gate', params: ['owner'] },
|
|
34
|
+
localLlm: { alias: 'test-paid-llm', params: ['cap'] },
|
|
35
|
+
updateBase: openProtocol(route('manager:update', '/update')),
|
|
36
|
+
})
|
|
37
|
+
const get = paid.project.branding.get
|
|
38
|
+
const save = paid.project.branding.save
|
|
39
|
+
|
|
40
|
+
expect(get.alias).toBe(connect.project.branding.get)
|
|
41
|
+
expect(save.alias).toBe(connect.project.branding.save)
|
|
42
|
+
expect(get.route.route.path).toBe('/project/:id/branding')
|
|
43
|
+
expect(save.route.route.path).toBe('/project/:id/branding')
|
|
44
|
+
expect(get.route.route.method).toBe(RouteMethod.GET)
|
|
45
|
+
expect(save.route.route.method).toBe(RouteMethod.POST)
|
|
46
|
+
expect(aliasOf(get.route.route.parent)).toBe(connect.base)
|
|
47
|
+
expect(aliasOf(save.route.route.parent)).toBe(connect.base)
|
|
48
|
+
// The paid local-LLM gate sits on its own two routes; branding carries no gate of its own and
|
|
49
|
+
// inherits only the base's guard and ownership gate.
|
|
50
|
+
expect(paid.project.llm.gate?.alias).toBe('test-paid-llm')
|
|
51
|
+
expect(get.gate).toBeUndefined()
|
|
52
|
+
expect(save.gate).toBeUndefined()
|
|
53
|
+
expect(connectRef.project.branding.save.alias).toBe(connect.project.branding.save)
|
|
54
|
+
expect(connectRef.project.branding.get.alias).toBe(connect.project.branding.get)
|
|
33
55
|
})
|
|
34
|
-
})
|
|
35
56
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const id = jobIdOf(ConnectJobKind.StoryDevelop, 'project-card', 'story-card')
|
|
57
|
+
test('the branding save body is a patch of strings that can never carry the credit', () => {
|
|
58
|
+
const validate = new Ajv({ strict: false }).compile(ConnectProjectBrandingSaveSchema)
|
|
39
59
|
|
|
40
|
-
expect(
|
|
41
|
-
expect(
|
|
42
|
-
|
|
43
|
-
})
|
|
60
|
+
expect(validate({})).toBe(true)
|
|
61
|
+
expect(validate({ googleTag: 'GTM-ABCD123', termsUrl: '/terms', privacyUrl: '' })).toBe(true)
|
|
62
|
+
expect(validate({ copyright: '' })).toBe(false)
|
|
63
|
+
expect(validate({ hideCredit: true })).toBe(false)
|
|
64
|
+
expect(validate({ googleTag: 'G-'.padEnd(CONNECT_BRANDING_GOOGLE_TAG_MAX + 1, 'X') })).toBe(false)
|
|
44
65
|
})
|
|
45
66
|
|
|
46
|
-
test('
|
|
47
|
-
expect(
|
|
48
|
-
|
|
49
|
-
|
|
67
|
+
test('keeps story mutations in planning and exposes only the composed story status', () => {
|
|
68
|
+
expect(Object.keys(tree.story)).toEqual(['status'])
|
|
69
|
+
expect(Object.keys(connect.story)).toEqual(['status'])
|
|
70
|
+
expect(Object.keys(connectRef.story)).toEqual(['status'])
|
|
71
|
+
expect(tree.story.status.route.route.path).toBe('/project/:id/story/:storyId/status')
|
|
50
72
|
})
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
expect(parseJobId(jobIdOf(ConnectJobKind.PipelineResume, 'p1', suffix)).suffix).toBe(suffix)
|
|
73
|
+
test('has no generic connector job endpoint', () => {
|
|
74
|
+
expect((tree.project as Record<string, unknown>).job).toBeUndefined()
|
|
75
|
+
expect((connect.project as Record<string, unknown>).job).toBeUndefined()
|
|
76
|
+
expect((connectRef.project as Record<string, unknown>).job).toBeUndefined()
|
|
56
77
|
})
|
|
57
78
|
})
|