@panaversity/ksor 0.0.39 → 0.0.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +965 -0
- package/README.md +29 -9
- package/dist/checker/check-main.mjs +14049 -0
- package/dist/cli.mjs +11528 -5213
- package/dist/gateway-api-CF4ED9_g-BQusM_dK.mjs +10895 -0
- package/dist/gateway.d.mts +52 -13
- package/dist/gateway.mjs +2 -2
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{src-pl4aOpVs.mjs → src-dqpI-p1a.mjs} +1 -0
- package/docs/authorization.md +8 -6
- package/docs/deploying.md +36 -25
- package/docs/index.md +26 -13
- package/docs/ingesting.md +70 -22
- package/docs/tool-surface.md +69 -16
- package/package.json +4 -3
- package/schema/migrations/2.4-2.5__okf-profile.sql +114 -0
- package/schema/schema.sql +77 -14
- package/templates/scaffold/.agents/skills/add-sources/SKILL.md +63 -18
- package/templates/scaffold/.agents/skills/format-checker/SKILL.md +42 -33
- package/templates/scaffold/.agents/skills/format-checker/check.mjs +13827 -1314
- package/templates/scaffold/.agents/skills/intake-interview/SKILL.md +65 -27
- package/templates/scaffold/.agents/skills/make-slides/SKILL.md +7 -5
- package/templates/scaffold/.agents/skills/make-summary/SKILL.md +13 -6
- package/templates/scaffold/.claude/skills/add-sources/SKILL.md +63 -18
- package/templates/scaffold/.claude/skills/format-checker/SKILL.md +42 -33
- package/templates/scaffold/.claude/skills/format-checker/check.mjs +13827 -1314
- package/templates/scaffold/.claude/skills/intake-interview/SKILL.md +65 -27
- package/templates/scaffold/.claude/skills/make-slides/SKILL.md +7 -5
- package/templates/scaffold/.claude/skills/make-summary/SKILL.md +13 -6
- package/templates/scaffold/.github/workflows/validate.yml +9 -1
- package/templates/scaffold/.ksor/governance.yaml +17 -0
- package/templates/scaffold/AGENTS.md +293 -119
- package/templates/scaffold/Dockerfile +5 -1
- package/templates/scaffold/README.md +160 -42
- package/templates/scaffold/env.example +37 -6
- package/templates/scaffold/gitignore +14 -6
- package/templates/scaffold/instance.md +21 -17
- package/templates/scaffold/knowledge/governance-ladder.md +11 -2
- package/templates/scaffold/knowledge/index.md +9 -0
- package/templates/scaffold/knowledge/surfaces/for-agents.md +13 -6
- package/templates/scaffold/knowledge/surfaces/for-people.md +7 -6
- package/templates/scaffold/knowledge/surfaces/index.md +4 -20
- package/templates/scaffold/knowledge/surfaces/overview.md +25 -0
- package/templates/scaffold/knowledge/what-is-a-ksor.md +6 -5
- package/templates/scaffold/knowledge/what-is-a-ksor.summary.md +4 -0
- package/templates/scaffold/package.json +3 -4
- package/templates/scaffold/pnpm-lock.yaml +3 -0
- package/templates/scaffold/system/gateways/content.ts +13 -0
- package/templates/scaffold/system/site/app/(home)/page.tsx +2 -2
- package/templates/scaffold/system/site/app/.well-known/mcp/server.json/route.ts +10 -0
- package/templates/scaffold/system/site/app/docs/[[...slug]]/page.tsx +134 -91
- package/templates/scaffold/system/site/app/global.css +198 -6
- package/templates/scaffold/system/site/app/layout.tsx +8 -3
- package/templates/scaffold/system/site/app/llms-full.txt/route.ts +13 -7
- package/templates/scaffold/system/site/app/llms.txt/route.ts +12 -7
- package/templates/scaffold/system/site/app/md/[[...slug]]/route.ts +26 -25
- package/templates/scaffold/system/site/components/code-block.tsx +87 -0
- package/templates/scaffold/system/site/components/embed.tsx +276 -0
- package/templates/scaffold/system/site/components/footer-mark.tsx +3 -2
- package/templates/scaffold/system/site/components/governance.tsx +219 -87
- package/templates/scaffold/system/site/components/mdx.tsx +10 -2
- package/templates/scaffold/system/site/components/record-index.tsx +5 -5
- package/templates/scaffold/system/site/components/record-stack.tsx +10 -9
- package/templates/scaffold/system/site/components/record-views.tsx +6 -17
- package/templates/scaffold/system/site/components/sidebar-status.tsx +19 -18
- package/templates/scaffold/system/site/lib/alert-rule.ts +214 -0
- package/templates/scaffold/system/site/lib/attachment-rule.ts +6 -1
- package/templates/scaffold/system/site/lib/attachments.ts +0 -28
- package/templates/scaffold/system/site/lib/audience-rule.ts +15 -21
- package/templates/scaffold/system/site/lib/audience.ts +42 -146
- package/templates/scaffold/system/site/lib/embed-rule.ts +255 -0
- package/templates/scaffold/system/site/lib/governance.ts +339 -225
- package/templates/scaffold/system/site/lib/index-routes.ts +125 -0
- package/templates/scaffold/system/site/lib/lifecycle-rule.ts +52 -0
- package/templates/scaffold/system/site/lib/lock.ts +282 -0
- package/templates/scaffold/system/site/lib/order-rule.ts +37 -0
- package/templates/scaffold/system/site/lib/record-href.ts +68 -0
- package/templates/scaffold/system/site/lib/record-link.tsx +26 -0
- package/templates/scaffold/system/site/lib/rules-version.ts +11 -0
- package/templates/scaffold/system/site/lib/shared.ts +67 -104
- package/templates/scaffold/system/site/lib/sim-rule.ts +49 -0
- package/templates/scaffold/system/site/lib/source.ts +256 -186
- package/templates/scaffold/system/site/lib/stage-knowledge.ts +638 -467
- package/templates/scaffold/system/site/lib/stage-manifest.ts +128 -0
- package/templates/scaffold/system/site/package.json +1 -0
- package/templates/scaffold/system/site/record/actor.ts +23 -0
- package/templates/scaffold/system/site/record/check.ts +571 -0
- package/templates/scaffold/system/site/record/citations.ts +312 -0
- package/templates/scaffold/system/site/record/frontmatter.ts +134 -0
- package/templates/scaffold/system/site/record/git-ledger.ts +171 -0
- package/templates/scaffold/system/site/record/hygiene.ts +320 -0
- package/templates/scaffold/system/site/record/index-file.ts +150 -0
- package/templates/scaffold/system/site/record/index.ts +103 -0
- package/templates/scaffold/system/site/record/instance.ts +257 -0
- package/templates/scaffold/system/site/record/instant.ts +43 -0
- package/templates/scaffold/system/site/record/ledger.ts +694 -0
- package/templates/scaffold/system/site/record/load.ts +129 -0
- package/templates/scaffold/system/site/record/lock.ts +306 -0
- package/templates/scaffold/system/site/record/near-miss.ts +37 -0
- package/templates/scaffold/system/site/record/policy.ts +414 -0
- package/templates/scaffold/system/site/record/profile.ts +535 -0
- package/templates/scaffold/system/site/record/refusal.ts +106 -0
- package/templates/scaffold/system/site/record/yaml-file.ts +103 -0
- package/templates/scaffold/system/site/source.config.ts +109 -23
- package/dist/gateway-api-CmIthmJS-IUA9qS-T.mjs +0 -3225
- package/templates/scaffold/system/site/lib/denial-rule.ts +0 -220
- package/templates/scaffold/system/site/lib/page-order.ts +0 -93
|
@@ -262,7 +262,7 @@ html > body[data-scroll-locked] {
|
|
|
262
262
|
/* The caution colour is a TOKEN, declared as a light/dark pair on the root
|
|
263
263
|
rather than inside the one class that first used it. Every surface that has
|
|
264
264
|
to say "the record withdrew this" now spends the same colour — the banner
|
|
265
|
-
over a
|
|
265
|
+
over a deprecated document, and the chip that marks it in the sidebar, the
|
|
266
266
|
listings, the front door and search. Declared in both themes deliberately:
|
|
267
267
|
a colour written only on `:root` leaks into dark, because `:root` and
|
|
268
268
|
`.dark` have equal specificity and whichever comes later wins. */
|
|
@@ -489,10 +489,10 @@ html > body[data-scroll-locked] {
|
|
|
489
489
|
}
|
|
490
490
|
|
|
491
491
|
/* A withdrawn document's status chip, wherever one renders. A `draft` chip and
|
|
492
|
-
a `
|
|
492
|
+
a `deprecated` chip were pixel-identical — the same hairline border and the
|
|
493
493
|
same muted text — so the two statuses that mean the most different things
|
|
494
494
|
looked the same at the moment a reader picks between two documents. The
|
|
495
|
-
colour is additive, never the whole signal: the word "
|
|
495
|
+
colour is additive, never the whole signal: the word "deprecated" is beside
|
|
496
496
|
it in every one of these places (GOV.UK's rule — never rely on colour alone).
|
|
497
497
|
Unlayered, so it beats the Tailwind utility that sets the chip's border and
|
|
498
498
|
text colour; the banner class above has always worked this way. */
|
|
@@ -519,8 +519,16 @@ html > body[data-scroll-locked] {
|
|
|
519
519
|
}
|
|
520
520
|
|
|
521
521
|
/* …and in search too, where a reader is choosing between results. The chip is
|
|
522
|
-
drawn by `content`, so the tone has to be set on the same pseudo-element.
|
|
523
|
-
|
|
522
|
+
drawn by `content`, so the tone has to be set on the same pseudo-element.
|
|
523
|
+
|
|
524
|
+
found live 2026-08-25: this selector still read `superseded`, the PRE-PROFILE
|
|
525
|
+
status word. Under the profile the withdrawn state is `deprecated` (record
|
|
526
|
+
spec §2.2), so it matched nothing and a withdrawn document's search row wore
|
|
527
|
+
the ordinary grey chip — on the one surface whose snippet quotes the
|
|
528
|
+
withdrawn figure, which is why the rule exists. The value here is
|
|
529
|
+
`lifecycleBadge`'s, not `status`'s; the two agree on this word and the badge
|
|
530
|
+
is what the map carries. */
|
|
531
|
+
[data-ksor-status="deprecated"]::after {
|
|
524
532
|
border-color: color-mix(in oklab, var(--ksor-caution) 45%, transparent);
|
|
525
533
|
color: var(--ksor-caution);
|
|
526
534
|
}
|
|
@@ -678,13 +686,53 @@ html > body[data-scroll-locked] {
|
|
|
678
686
|
font-weight: 500;
|
|
679
687
|
letter-spacing: 0.14em;
|
|
680
688
|
text-transform: uppercase;
|
|
681
|
-
|
|
689
|
+
/* The record's own accent, at reading strength. The labels were grey on
|
|
690
|
+
near-white and the head did not read as a head at all — on a three-column
|
|
691
|
+
table the eye had to work out which row was the label. */
|
|
692
|
+
color: color-mix(in oklab, var(--color-fd-primary) 72%, var(--color-fd-foreground));
|
|
693
|
+
background: color-mix(in oklab, var(--color-fd-primary) 7%, var(--color-fd-muted));
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/* The head is a band, so it is ruled off from the body rather than floating
|
|
697
|
+
above it — and the rule is the accent's, not the hairline's, so the two read
|
|
698
|
+
as one object. */
|
|
699
|
+
#nd-page .prose thead {
|
|
700
|
+
border-bottom: 2px solid color-mix(in oklab, var(--color-fd-primary) 28%, var(--color-fd-border));
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
.dark #nd-page .prose thead th {
|
|
704
|
+
/* Against near-black the same 7% is almost nothing, and the same 72% ink is
|
|
705
|
+
too bright to sit under body text. */
|
|
706
|
+
color: color-mix(in oklab, var(--color-fd-primary) 60%, var(--color-fd-foreground));
|
|
707
|
+
background: color-mix(in oklab, var(--color-fd-primary) 12%, var(--color-fd-muted));
|
|
682
708
|
}
|
|
683
709
|
|
|
684
710
|
#nd-page .prose tbody tr {
|
|
685
711
|
border-bottom: 1px solid var(--color-fd-border);
|
|
686
712
|
}
|
|
687
713
|
|
|
714
|
+
/* And every other row carries a wash. A hairline alone is enough on a
|
|
715
|
+
two-column table; across three columns of wrapped prose the eye loses which
|
|
716
|
+
cells belong together, because the gap between rows and the gap between
|
|
717
|
+
lines INSIDE a cell are the same distance. The band is what holds a row
|
|
718
|
+
together over that distance.
|
|
719
|
+
|
|
720
|
+
Faint on purpose: it groups, it does not label. Anything stronger competes
|
|
721
|
+
with the head, which is the row that is meant to be read first. */
|
|
722
|
+
#nd-page .prose tbody tr:nth-child(even) {
|
|
723
|
+
background: var(--color-fd-background);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
#nd-page .prose tbody tr:nth-child(odd) {
|
|
727
|
+
background: color-mix(in oklab, var(--color-fd-border) 14%, var(--color-fd-muted));
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
.dark #nd-page .prose tbody tr:nth-child(odd) {
|
|
731
|
+
/* A step of the same SIZE reads much stronger against near-black, so the
|
|
732
|
+
dark pair takes less of the border to land at the same apparent distance. */
|
|
733
|
+
background: color-mix(in oklab, var(--color-fd-border) 8%, var(--color-fd-muted));
|
|
734
|
+
}
|
|
735
|
+
|
|
688
736
|
#nd-page .prose tbody tr:last-child {
|
|
689
737
|
border-bottom: 0;
|
|
690
738
|
}
|
|
@@ -731,6 +779,117 @@ html > body[data-scroll-locked] {
|
|
|
731
779
|
background: var(--color-fd-muted);
|
|
732
780
|
}
|
|
733
781
|
|
|
782
|
+
/* A block with no language is not code — it is a passage to reproduce
|
|
783
|
+
verbatim: a prompt to type, a command to say, a message to paste. This
|
|
784
|
+
record has eight, and they arrived as walls of tight monospace, set for
|
|
785
|
+
scanning columns of code when what is in them is sentences.
|
|
786
|
+
|
|
787
|
+
The highlighter's own output is the selector, so nothing has to be authored
|
|
788
|
+
and nothing changes for the record: shiki gives every token in a HIGHLIGHTED
|
|
789
|
+
block an inline `--shiki-light`, and a plain one has none. `:has()` asks
|
|
790
|
+
that question directly.
|
|
791
|
+
|
|
792
|
+
Prose leading, because the lines are sentences; and the accent down the left
|
|
793
|
+
edge, which is what the rest of this site uses to mark structure — here it
|
|
794
|
+
says "reproduce this exactly", which is the one thing these blocks have in
|
|
795
|
+
common. */
|
|
796
|
+
#nd-page .prose figure.shiki:not(:has(code span[style*="--shiki-light"])) {
|
|
797
|
+
border-left: 3px solid color-mix(in oklab, var(--color-fd-primary) 55%, var(--color-fd-border));
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
#nd-page .prose figure.shiki:not(:has(code span[style*="--shiki-light"])) code {
|
|
801
|
+
line-height: 1.7;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/* Showing the whole of a long line, when the reader asks for it.
|
|
805
|
+
components/code-block.tsx puts the button on the block and sets the
|
|
806
|
+
attribute; this is what the attribute means.
|
|
807
|
+
|
|
808
|
+
`pre` is `w-max` in the shell, which sizes it to the longest line — that is
|
|
809
|
+
what makes wrapping impossible, so the width has to come back to the column
|
|
810
|
+
before `pre-wrap` can do anything. A wrapped line is then indented under its
|
|
811
|
+
own, the way a terminal marks a continuation, because otherwise a two-line
|
|
812
|
+
command reads as two commands.
|
|
813
|
+
|
|
814
|
+
Wrapping is NOT the default. It was, for one commit, and it is worse for the
|
|
815
|
+
blocks that do not need it: a yaml file rewrapped at the column reads as
|
|
816
|
+
though its indentation means something else. */
|
|
817
|
+
#nd-page .prose figure.shiki[data-wrapped] pre {
|
|
818
|
+
width: 100%;
|
|
819
|
+
min-width: 0;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
#nd-page .prose figure.shiki[data-wrapped] code > * {
|
|
823
|
+
white-space: pre-wrap;
|
|
824
|
+
overflow-wrap: anywhere;
|
|
825
|
+
/* The hang, plus the gutter below — so a continuation is indented from the
|
|
826
|
+
text, not from the block's edge. */
|
|
827
|
+
padding-left: calc(1rem + 2ch);
|
|
828
|
+
text-indent: -2ch;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/* A block needs a gutter on both sides. Unwrapped, the shell gives one on the
|
|
832
|
+
left with `ps-*` and lets a long line run under the buttons on the right,
|
|
833
|
+
because the line can always be scrolled clear of them. Wrapped, it cannot:
|
|
834
|
+
the first line ends under the copy button with no way to move it, and the
|
|
835
|
+
left edge sits on the rule (seen live). So the wrapped state pays for its
|
|
836
|
+
own margins — a gutter left, and enough right for two 24px buttons. */
|
|
837
|
+
#nd-page .prose figure.shiki[data-wrapped] code {
|
|
838
|
+
padding-inline: 0 4rem;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
#nd-page .prose figure.shiki[data-wrapped] .fd-scroll-container {
|
|
842
|
+
padding-inline-start: 0;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/* And a hairline where a scrollbar appears, in place of the platform's slab. */
|
|
846
|
+
#nd-page .prose figure.shiki .fd-scroll-container {
|
|
847
|
+
scrollbar-width: thin;
|
|
848
|
+
scrollbar-color: var(--color-fd-border) transparent;
|
|
849
|
+
}
|
|
850
|
+
/* A callout has to look like a callout. The same `bg-fd-card` trap as the code
|
|
851
|
+
block above: fumadocs paints the panel with `--card`, which in this palette
|
|
852
|
+
is all but the page colour, so a Note and a Warning both arrived as a white
|
|
853
|
+
box with a coloured hair down one edge — the kind was legible only in the
|
|
854
|
+
icon.
|
|
855
|
+
|
|
856
|
+
Fumadocs already puts the kind's own colour on the element as
|
|
857
|
+
`--callout-color`, so ONE rule tints every kind: a wash of that colour over
|
|
858
|
+
`--muted`, the token that means "a surface on the page". No per-kind
|
|
859
|
+
selector, and a kind added upstream is tinted without touching this.
|
|
860
|
+
|
|
861
|
+
6% in light and 10% in dark, because the same fraction reads as a different
|
|
862
|
+
amount of colour against near-white and against near-black. Kept low on
|
|
863
|
+
purpose: this is the record's own accent discipline — the tint says which
|
|
864
|
+
kind, it does not compete with the prose. */
|
|
865
|
+
#nd-page .prose [style*="--callout-color"] {
|
|
866
|
+
background: color-mix(in oklab, var(--callout-color) 6%, var(--color-fd-muted));
|
|
867
|
+
border-color: color-mix(in oklab, var(--callout-color) 20%, var(--color-fd-border));
|
|
868
|
+
/* The shadow was doing the work the colour now does, and lifted the panel
|
|
869
|
+
off the page in a way nothing else on it does. */
|
|
870
|
+
box-shadow: none;
|
|
871
|
+
|
|
872
|
+
/* THE RULE DOWN THE LEFT EDGE — the same device the verbatim block uses, so
|
|
873
|
+
"this passage is set apart" looks the same everywhere it happens, whatever
|
|
874
|
+
kind of thing is set apart. Fumadocs draws a half-pixel bar INSIDE the
|
|
875
|
+
panel instead, which is too faint to read as a marker at all.
|
|
876
|
+
|
|
877
|
+
In the kind's own colour, which for a Note — much the commonest — is the
|
|
878
|
+
accent, so the standard case is the blue line. */
|
|
879
|
+
border-left: 3px solid color-mix(in oklab, var(--callout-color) 65%, var(--color-fd-border));
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/* And the inner bar goes, now that the edge carries it. Two rules for the same
|
|
883
|
+
job, a pixel apart, read as a mistake. */
|
|
884
|
+
#nd-page .prose [style*="--callout-color"] > div[class*="w-0.5"] {
|
|
885
|
+
display: none;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
.dark #nd-page .prose [style*="--callout-color"] {
|
|
889
|
+
background: color-mix(in oklab, var(--callout-color) 10%, var(--color-fd-muted));
|
|
890
|
+
border-color: color-mix(in oklab, var(--callout-color) 24%, var(--color-fd-border));
|
|
891
|
+
}
|
|
892
|
+
|
|
734
893
|
/* Re-assert the shell's own reset, which the ramp above broke. The prose
|
|
735
894
|
plugin zeroes the first block's top margin, but writes that rule as
|
|
736
895
|
`.prose :where(> :first-child)`, and `:where()` contributes nothing to
|
|
@@ -743,6 +902,39 @@ html > body[data-scroll-locked] {
|
|
|
743
902
|
margin-top: 0;
|
|
744
903
|
}
|
|
745
904
|
|
|
905
|
+
/* A numbered list is a sequence, and its numbers should say so. The marker
|
|
906
|
+
arrived grey at regular weight, lighter than the text it counts, so a list
|
|
907
|
+
of six steps read as six paragraphs that happened to start with digits.
|
|
908
|
+
|
|
909
|
+
The accent, and only here: a number is structure, which is what this
|
|
910
|
+
record's accent is for. */
|
|
911
|
+
#nd-page .prose ol > li::marker {
|
|
912
|
+
color: var(--color-fd-primary);
|
|
913
|
+
font-weight: 600;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/* And the TERM a list item defines. `**Heartbeat:** a schedule that…` is the
|
|
917
|
+
commonest shape in this record — a term, then what it means — and the term
|
|
918
|
+
is the thing a reader scans for.
|
|
919
|
+
|
|
920
|
+
Only the term. Bold elsewhere in the sentence stays ink, because a page
|
|
921
|
+
where every emphasis is coloured has no emphasis: the accent stops meaning
|
|
922
|
+
"this is the one to find" and starts meaning "this is bold". */
|
|
923
|
+
#nd-page .prose li > strong:first-child,
|
|
924
|
+
#nd-page .prose li > p:first-child > strong:first-child {
|
|
925
|
+
color: var(--color-fd-primary);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/* The same rule, one level in. The document's first block is not a child of
|
|
929
|
+
`.prose` — it is inside the views wrapper, and inside a tab panel when the
|
|
930
|
+
document has a summary — so the reset above never reached it and the top of
|
|
931
|
+
every document sat 20px lower than the rule above it intended (measured
|
|
932
|
+
after the empty view strip was removed, which is what exposed it). */
|
|
933
|
+
#nd-page .prose > .ksor-views > :first-child,
|
|
934
|
+
#nd-page .prose > .ksor-views [role="tabpanel"] > :first-child {
|
|
935
|
+
margin-top: 0;
|
|
936
|
+
}
|
|
937
|
+
|
|
746
938
|
/* ── The page pager ──────────────────────────────────────────────────────────
|
|
747
939
|
Fumadocs lays the neighbours out as a grid that becomes two columns when both
|
|
748
940
|
exist. With only one — the first or last document of the record — it stays a
|
|
@@ -2,7 +2,8 @@ import { RootProvider } from "fumadocs-ui/provider/next";
|
|
|
2
2
|
import "./global.css";
|
|
3
3
|
import type { Metadata } from "next";
|
|
4
4
|
import { appTitle } from "@/lib/shared";
|
|
5
|
-
import { basePath,
|
|
5
|
+
import { basePath, badgeByUrl } from "@/lib/source";
|
|
6
|
+
import { readStageManifest } from "@/lib/stage-manifest";
|
|
6
7
|
import KsorSearchDialog from "@/components/search-dialog";
|
|
7
8
|
|
|
8
9
|
// No next/font/google: it fetches the face from Google at BUILD time, so a
|
|
@@ -17,13 +18,17 @@ export const metadata: Metadata = {
|
|
|
17
18
|
template: `%s | ${appTitle}`,
|
|
18
19
|
},
|
|
19
20
|
description: "The Knowledge System of Record for humans and AI agents.",
|
|
21
|
+
// A build that shows drafts (`KSOR_DRAFTS=show`) is a preview, and a static
|
|
22
|
+
// site's pages are open-web artefacts: it says so to every crawler rather
|
|
23
|
+
// than letting a draft be indexed under the record's name (build spec §3).
|
|
24
|
+
...(readStageManifest().drafts === "shown" ? { robots: { index: false, follow: false } } : {}),
|
|
20
25
|
};
|
|
21
26
|
|
|
22
27
|
export default function Layout({ children }: LayoutProps<"/">) {
|
|
23
28
|
return (
|
|
24
29
|
<html lang="en" suppressHydrationWarning>
|
|
25
30
|
<body className="flex flex-col min-h-screen">
|
|
26
|
-
{/* Which documents carry a
|
|
31
|
+
{/* Which documents carry a badge, for the search dialog — it
|
|
27
32
|
runs in the browser over a static index that has no field for it.
|
|
28
33
|
Delivered in the document rather than as a dialog prop because
|
|
29
34
|
RootProvider types `options` against the SHIPPED dialog's props, and
|
|
@@ -34,7 +39,7 @@ export default function Layout({ children }: LayoutProps<"/">) {
|
|
|
34
39
|
type="application/json"
|
|
35
40
|
id="ksor-statuses"
|
|
36
41
|
dangerouslySetInnerHTML={{
|
|
37
|
-
__html: JSON.stringify(
|
|
42
|
+
__html: JSON.stringify(badgeByUrl()).replaceAll("<", "\\u003c"),
|
|
38
43
|
}}
|
|
39
44
|
/>
|
|
40
45
|
<RootProvider
|
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
import { getLLMText,
|
|
1
|
+
import { getLLMText, getMachinePages } from "@/lib/source";
|
|
2
|
+
import { stampLines } from "@/lib/governance";
|
|
3
|
+
import { appName, appTitle } from "@/lib/shared";
|
|
4
|
+
import { readStageManifest } from "@/lib/stage-manifest";
|
|
2
5
|
|
|
3
6
|
export const revalidate = false;
|
|
4
7
|
|
|
8
|
+
/**
|
|
9
|
+
* The whole machine-admitted record in one file, opened by the stamps that
|
|
10
|
+
* connect it to one publication (R14) — so a consumer holding a copy can say
|
|
11
|
+
* which build it came from — then every document with its own governance
|
|
12
|
+
* block, exactly as its twin serves it.
|
|
13
|
+
*/
|
|
5
14
|
export async function GET(): Promise<Response> {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const scanned = await Promise.all(pages.map((page) => getLLMText(page, pages)));
|
|
10
|
-
|
|
11
|
-
return new Response(scanned.join("\n\n"));
|
|
15
|
+
const head = `---\ntitle: ${JSON.stringify(appTitle)}\nname: ${appName}\n${stampLines(readStageManifest().stamps).join("\n")}\n---`;
|
|
16
|
+
const scanned = getMachinePages().map((page) => getLLMText(page));
|
|
17
|
+
return new Response([head, ...scanned].join("\n\n"));
|
|
12
18
|
}
|
|
@@ -3,15 +3,20 @@ import { recordIndexText } from "@/lib/source";
|
|
|
3
3
|
export const revalidate = false;
|
|
4
4
|
|
|
5
5
|
// The agent-facing index of the record: this instance's name, then every
|
|
6
|
-
// document in sidebar order, each link usable
|
|
7
|
-
//
|
|
6
|
+
// document a MACHINE surface may carry, in sidebar order, each link usable
|
|
7
|
+
// as-is on a sub-path host.
|
|
8
8
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
9
|
+
// There is no caveat marker any more, because there is nothing to mark: the
|
|
10
|
+
// §2.5 table admits only stable, effective, unexpired, undenied concepts here,
|
|
11
|
+
// so a draft, a deprecated document and one past its `stale_after` are not
|
|
12
|
+
// entries at all. That replaced marking with exclusion — the older worry was a
|
|
13
|
+
// withdrawn document and its replacement sitting adjacent, told apart only by
|
|
14
|
+
// whatever a human typed into a title (research/site-design.md F1), and an
|
|
15
|
+
// agent picking either.
|
|
12
16
|
//
|
|
13
|
-
// The
|
|
14
|
-
//
|
|
17
|
+
// The set is decided ONCE, by staging, and read back here: the bytes are built
|
|
18
|
+
// in lib/source (`recordIndexText`) because the home page shows this same index
|
|
19
|
+
// to a reader — one index, one spelling.
|
|
15
20
|
export function GET(): Response {
|
|
16
21
|
return new Response(recordIndexText());
|
|
17
22
|
}
|
|
@@ -1,36 +1,38 @@
|
|
|
1
1
|
import { notFound } from "next/navigation";
|
|
2
2
|
|
|
3
|
-
import { getLLMText,
|
|
3
|
+
import { getLLMText, getMachinePages, rootIndexTwin, source } from "@/lib/source";
|
|
4
|
+
import { machineAdmits } from "@/lib/stage-manifest";
|
|
4
5
|
|
|
5
6
|
export const revalidate = false;
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
|
-
* Every document as markdown, at a stable address derived
|
|
9
|
-
* `/docs/policies/purchase-approval` is also served at
|
|
10
|
-
* `/md/policies/purchase-approval.md
|
|
9
|
+
* Every MACHINE-admitted document as markdown, at a stable address derived
|
|
10
|
+
* from its path: `/docs/policies/purchase-approval` is also served at
|
|
11
|
+
* `/md/policies/purchase-approval.md`, and the record's own index at
|
|
12
|
+
* `/md/index.md` — the one index with a twin (build spec §3).
|
|
11
13
|
*
|
|
12
14
|
* The record already IS markdown; without this an agent handed a document URL
|
|
13
15
|
* had to scrape a React app to reach text the record holds verbatim
|
|
14
|
-
* (research/site-design.md F2). The body carries the document's governance
|
|
15
|
-
* frontmatter, exactly as `llms-full.txt` does
|
|
16
|
-
* document knows its status, owner, sources and successor.
|
|
16
|
+
* (research/site-design.md F2). The body carries the document's governance and
|
|
17
|
+
* the build's stamps as frontmatter, exactly as `llms-full.txt` does.
|
|
17
18
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
19
|
+
* A page the machine surfaces decline — a draft, a deprecated, a not-yet-
|
|
20
|
+
* effective or a stale concept (record spec §2.5) — has no twin at all: the
|
|
21
|
+
* decision is staging's, read back here, never re-derived.
|
|
22
|
+
*
|
|
23
|
+
* Why a `/md/` prefix rather than appending `.md` to the document's own URL:
|
|
24
|
+
* under `output: "export"` a Route Handler cannot share a route segment with a
|
|
25
|
+
* Page, and there is no middleware to rewrite one onto the other. The prefix
|
|
22
26
|
* survives a static host, and the page advertises it in a `rel="alternate"`
|
|
23
|
-
* link so a consumer discovers it rather than guessing.
|
|
24
|
-
* canonical URL becomes possible the day a build emits these artifacts itself.
|
|
27
|
+
* link so a consumer discovers it rather than guessing.
|
|
25
28
|
*/
|
|
26
29
|
export function generateStaticParams(): { slug: string[] }[] {
|
|
27
|
-
|
|
28
|
-
const segments =
|
|
29
|
-
const last = segments.at(-1);
|
|
30
|
-
return {
|
|
31
|
-
slug: last === undefined ? ["index.md"] : [...segments.slice(0, -1), `${last}.md`],
|
|
32
|
-
};
|
|
30
|
+
const twins = getMachinePages().map((page) => {
|
|
31
|
+
const segments = page.url.replace(/^\/docs\/?/, "").split("/");
|
|
32
|
+
const last = segments.at(-1) ?? "";
|
|
33
|
+
return { slug: [...segments.slice(0, -1), `${last}.md`] };
|
|
33
34
|
});
|
|
35
|
+
return [{ slug: ["index.md"] }, ...twins];
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
export async function GET(
|
|
@@ -40,12 +42,11 @@ export async function GET(
|
|
|
40
42
|
const { slug = [] } = await params;
|
|
41
43
|
const last = slug.at(-1);
|
|
42
44
|
if (last === undefined || !last.endsWith(".md")) notFound();
|
|
43
|
-
const
|
|
45
|
+
const headers = { "content-type": "text/markdown; charset=utf-8" };
|
|
46
|
+
if (slug.length === 1 && last === "index.md") return new Response(rootIndexTwin(), { headers });
|
|
44
47
|
|
|
45
|
-
const page = source.getPage(
|
|
46
|
-
if (!page) notFound();
|
|
48
|
+
const page = source.getPage([...slug.slice(0, -1), last.slice(0, -".md".length)]);
|
|
49
|
+
if (!page || !machineAdmits(page.path)) notFound();
|
|
47
50
|
|
|
48
|
-
return new Response(
|
|
49
|
-
headers: { "content-type": "text/markdown; charset=utf-8" },
|
|
50
|
-
});
|
|
51
|
+
return new Response(getLLMText(page), { headers });
|
|
51
52
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { CodeBlock, Pre } from "fumadocs-ui/components/codeblock";
|
|
4
|
+
import { WrapText } from "lucide-react";
|
|
5
|
+
import { useCallback, useEffect, useRef, useState, type ReactElement } from "react";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A code block that can be asked to show the whole line.
|
|
9
|
+
*
|
|
10
|
+
* A record's commands are long — one in the fixture runs to 292 characters —
|
|
11
|
+
* and in a 672px column that is a scrollbar, with the left edge of every line
|
|
12
|
+
* going out of view as you drag it. Wrapping everything by default was tried
|
|
13
|
+
* and is worse for the blocks that do not need it: a yaml file rewrapped at
|
|
14
|
+
* the column reads as though its indentation means something else.
|
|
15
|
+
*
|
|
16
|
+
* So the reader decides, per block. The button appears ONLY on a block that
|
|
17
|
+
* actually overflows, which is measured after layout rather than guessed —
|
|
18
|
+
* most blocks fit, and a control that does nothing is worse than no control.
|
|
19
|
+
*
|
|
20
|
+
* The `Actions` slot is fumadocs' own, so the button sits with the copy button
|
|
21
|
+
* and inherits its placement rather than being positioned against it.
|
|
22
|
+
*/
|
|
23
|
+
export function WrappableCodeBlock(props: React.ComponentProps<"pre">): ReactElement {
|
|
24
|
+
const figure = useRef<HTMLElement>(null);
|
|
25
|
+
const [overflows, setOverflows] = useState(false);
|
|
26
|
+
const [wrapped, setWrapped] = useState(false);
|
|
27
|
+
|
|
28
|
+
const measure = useCallback((): void => {
|
|
29
|
+
const viewport = figure.current?.querySelector<HTMLElement>(".fd-scroll-container");
|
|
30
|
+
if (!viewport) return;
|
|
31
|
+
// While wrapped there is nothing to overflow, so the question is only
|
|
32
|
+
// asked in the state that can answer it — otherwise turning wrapping on
|
|
33
|
+
// would remove the button that turns it off.
|
|
34
|
+
setOverflows((was) => (wrapped ? was : viewport.scrollWidth > viewport.clientWidth + 1));
|
|
35
|
+
}, [wrapped]);
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
measure();
|
|
39
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
40
|
+
const viewport = figure.current?.querySelector<HTMLElement>(".fd-scroll-container");
|
|
41
|
+
if (!viewport) return;
|
|
42
|
+
// The column changes width with the window, and with the table of contents
|
|
43
|
+
// appearing; a block that fits at one width overflows at another.
|
|
44
|
+
const observer = new ResizeObserver(measure);
|
|
45
|
+
observer.observe(viewport);
|
|
46
|
+
return () => observer.disconnect();
|
|
47
|
+
}, [measure]);
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<CodeBlock
|
|
51
|
+
ref={figure}
|
|
52
|
+
data-wrapped={wrapped ? "" : undefined}
|
|
53
|
+
Actions={({ className, children }) => (
|
|
54
|
+
// `children` IS the copy button — fumadocs hands the default actions
|
|
55
|
+
// in, and a slot that ignores them REPLACES the copy button instead of
|
|
56
|
+
// joining it (found live: the block lost copy entirely). The wrapper
|
|
57
|
+
// mirrors the default's own `empty:hidden`, so a block with neither
|
|
58
|
+
// button still renders nothing.
|
|
59
|
+
<div className={`flex items-center empty:hidden ${className ?? ""}`}>
|
|
60
|
+
{children}
|
|
61
|
+
{overflows ? (
|
|
62
|
+
<button
|
|
63
|
+
type="button"
|
|
64
|
+
aria-pressed={wrapped}
|
|
65
|
+
onClick={() => setWrapped((on) => !on)}
|
|
66
|
+
// The copy button's own metrics — measured, not guessed: 24x24
|
|
67
|
+
// with a 3.5 icon. Mine came out 30x30 and the pair read as two
|
|
68
|
+
// controls of different importance.
|
|
69
|
+
className="inline-flex size-6 items-center justify-center rounded-lg transition-colors hover:bg-fd-accent hover:text-fd-accent-foreground [&_svg]:size-3.5"
|
|
70
|
+
// Said as what it does, not as what it is: the reader wants the
|
|
71
|
+
// rest of the line, not a setting.
|
|
72
|
+
title={wrapped ? "Show one line per line" : "Show the whole line"}
|
|
73
|
+
>
|
|
74
|
+
<WrapText aria-hidden />
|
|
75
|
+
<span className="sr-only">
|
|
76
|
+
{wrapped ? "Show one line per line" : "Show the whole line"}
|
|
77
|
+
</span>
|
|
78
|
+
</button>
|
|
79
|
+
) : null}
|
|
80
|
+
</div>
|
|
81
|
+
)}
|
|
82
|
+
{...props}
|
|
83
|
+
>
|
|
84
|
+
<Pre>{props.children}</Pre>
|
|
85
|
+
</CodeBlock>
|
|
86
|
+
);
|
|
87
|
+
}
|